1#[derive(Debug)]
52pub struct SerializationError {
53 pub fallback_data: String,
55 pub error: Box<dyn std::error::Error + Send + Sync>,
57}
58
59impl SerializationError {
60 pub fn new(
62 fallback_data: String,
63 error: impl std::error::Error + Send + Sync + 'static,
64 ) -> Self {
65 Self {
66 fallback_data,
67 error: Box::new(error),
68 }
69 }
70}
71
72impl std::fmt::Display for SerializationError {
73 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
74 write!(f, "Serialization failed: {}", self.error)
75 }
76}
77
78impl std::error::Error for SerializationError {
79 fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
80 Some(self.error.as_ref())
81 }
82}
83
84use chrono::{DateTime, Utc};
85use metrics::{counter, histogram};
86use outlet::{RequestData, RequestHandler, ResponseData};
87use serde::{Deserialize, Serialize};
88use serde_json::Value;
89use sqlx::PgPool;
90use std::collections::HashMap;
91use std::sync::Arc;
92use std::time::{Instant, SystemTime};
93use tracing::{debug, error, instrument, warn};
94use uuid::Uuid;
95
96pub mod error;
97pub mod repository;
98pub use error::PostgresHandlerError;
99pub use repository::{
100 HttpRequest, HttpResponse, RequestFilter, RequestRepository, RequestResponsePair,
101};
102
103pub use sqlx_pool_router::{DbPools, PoolProvider, TestDbPools};
105
106pub fn migrator() -> sqlx::migrate::Migrator {
130 sqlx::migrate!("./migrations")
131}
132
133type RequestSerializer<T> =
138 Arc<dyn Fn(&outlet::RequestData) -> Result<T, SerializationError> + Send + Sync>;
139
140type ResponseSerializer<T> = Arc<
146 dyn Fn(&outlet::RequestData, &outlet::ResponseData) -> Result<T, SerializationError>
147 + Send
148 + Sync,
149>;
150
151#[derive(Clone)]
162pub struct PostgresHandler<P = PgPool, TReq = Value, TRes = Value>
163where
164 P: PoolProvider,
165 TReq: for<'de> Deserialize<'de> + Serialize + Send + Sync + 'static,
166 TRes: for<'de> Deserialize<'de> + Serialize + Send + Sync + 'static,
167{
168 pool: P,
169 request_serializer: RequestSerializer<TReq>,
170 response_serializer: ResponseSerializer<TRes>,
171 instance_id: Uuid,
172}
173
174impl<P, TReq, TRes> PostgresHandler<P, TReq, TRes>
175where
176 P: PoolProvider,
177 TReq: for<'de> Deserialize<'de> + Serialize + Send + Sync + 'static,
178 TRes: for<'de> Deserialize<'de> + Serialize + Send + Sync + 'static,
179{
180 fn default_request_serializer() -> RequestSerializer<TReq> {
183 Arc::new(|request_data| {
184 let bytes = request_data.body.as_deref().unwrap_or(&[]);
185 serde_json::from_slice::<TReq>(bytes).map_err(|error| {
186 let fallback_data = String::from_utf8_lossy(bytes).to_string();
187 SerializationError::new(fallback_data, error)
188 })
189 })
190 }
191
192 fn default_response_serializer() -> ResponseSerializer<TRes> {
195 Arc::new(|_request_data, response_data| {
196 let bytes = response_data.body.as_deref().unwrap_or(&[]);
197 serde_json::from_slice::<TRes>(bytes).map_err(|error| {
198 let fallback_data = String::from_utf8_lossy(bytes).to_string();
199 SerializationError::new(fallback_data, error)
200 })
201 })
202 }
203
204 pub fn with_request_serializer<F>(mut self, serializer: F) -> Self
216 where
217 F: Fn(&outlet::RequestData) -> Result<TReq, SerializationError> + Send + Sync + 'static,
218 {
219 self.request_serializer = Arc::new(serializer);
220 self
221 }
222
223 pub fn with_response_serializer<F>(mut self, serializer: F) -> Self
235 where
236 F: Fn(&outlet::RequestData, &outlet::ResponseData) -> Result<TRes, SerializationError>
237 + Send
238 + Sync
239 + 'static,
240 {
241 self.response_serializer = Arc::new(serializer);
242 self
243 }
244
245 pub async fn from_pool_provider(pool_provider: P) -> Result<Self, PostgresHandlerError> {
285 Ok(Self {
286 pool: pool_provider,
287 request_serializer: Self::default_request_serializer(),
288 response_serializer: Self::default_response_serializer(),
289 instance_id: Uuid::new_v4(),
290 })
291 }
292
293 fn headers_to_json(headers: &HashMap<String, Vec<bytes::Bytes>>) -> Value {
295 let mut header_map = HashMap::new();
296 for (name, values) in headers {
297 if values.len() == 1 {
298 let value_str = String::from_utf8_lossy(&values[0]).to_string();
299 header_map.insert(name.clone(), Value::String(value_str));
300 } else {
301 let value_array: Vec<Value> = values
302 .iter()
303 .map(|v| Value::String(String::from_utf8_lossy(v).to_string()))
304 .collect();
305 header_map.insert(name.clone(), Value::Array(value_array));
306 }
307 }
308 serde_json::to_value(header_map).unwrap_or(Value::Null)
309 }
310
311 fn request_body_to_json_with_fallback(
313 &self,
314 request_data: &outlet::RequestData,
315 ) -> (Value, bool) {
316 match (self.request_serializer)(request_data) {
317 Ok(typed_value) => {
318 if let Ok(json_value) = serde_json::to_value(&typed_value) {
319 (json_value, true)
320 } else {
321 (
323 Value::String(
324 serde_json::to_string(&typed_value)
325 .expect("Serialized value must be convertible to JSON string"),
326 ),
327 false,
328 )
329 }
330 }
331 Err(serialization_error) => (Value::String(serialization_error.fallback_data), false),
332 }
333 }
334
335 fn response_body_to_json_with_fallback(
337 &self,
338 request_data: &outlet::RequestData,
339 response_data: &outlet::ResponseData,
340 ) -> (Value, bool) {
341 match (self.response_serializer)(request_data, response_data) {
342 Ok(typed_value) => {
343 if let Ok(json_value) = serde_json::to_value(&typed_value) {
344 (json_value, true)
345 } else {
346 (
348 Value::String(
349 serde_json::to_string(&typed_value)
350 .expect("Serialized value must be convertible to JSON string"),
351 ),
352 false,
353 )
354 }
355 }
356 Err(serialization_error) => (Value::String(serialization_error.fallback_data), false),
357 }
358 }
359
360 pub fn repository(&self) -> crate::repository::RequestRepository<P, TReq, TRes> {
365 crate::repository::RequestRepository::new(self.pool.clone())
366 }
367}
368
369impl<TReq, TRes> PostgresHandler<PgPool, TReq, TRes>
371where
372 TReq: for<'de> Deserialize<'de> + Serialize + Send + Sync + 'static,
373 TRes: for<'de> Deserialize<'de> + Serialize + Send + Sync + 'static,
374{
375 pub async fn new(database_url: &str) -> Result<Self, PostgresHandlerError> {
408 let pool = PgPool::connect(database_url)
409 .await
410 .map_err(PostgresHandlerError::Connection)?;
411
412 Ok(Self {
413 pool,
414 request_serializer: Self::default_request_serializer(),
415 response_serializer: Self::default_response_serializer(),
416 instance_id: Uuid::new_v4(),
417 })
418 }
419
420 pub async fn from_pool(pool: PgPool) -> Result<Self, PostgresHandlerError> {
455 Self::from_pool_provider(pool).await
456 }
457}
458
459impl<P, TReq, TRes> RequestHandler for PostgresHandler<P, TReq, TRes>
460where
461 P: PoolProvider,
462 TReq: for<'de> Deserialize<'de> + Serialize + Send + Sync + 'static,
463 TRes: for<'de> Deserialize<'de> + Serialize + Send + Sync + 'static,
464{
465 #[instrument(name = "outlet.handle_request", skip(self, data), fields(correlation_id = %data.correlation_id))]
466 async fn handle_request(&self, data: RequestData) {
467 let headers_json = Self::headers_to_json(&data.headers);
468 let (body_json, parsed) = if data.body.is_some() {
469 let (json, parsed) = self.request_body_to_json_with_fallback(&data);
470 (Some(json), parsed)
471 } else {
472 (None, false)
473 };
474
475 let timestamp: DateTime<Utc> = data.timestamp.into();
476
477 let query_start = Instant::now();
478 let result = sqlx::query(
479 r#"
480 INSERT INTO http_requests (instance_id, correlation_id, timestamp, method, uri, headers, body, body_parsed, trace_id, span_id)
481 VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10)
482 "#,
483 )
484 .bind(self.instance_id)
485 .bind(data.correlation_id as i64)
486 .bind(timestamp)
487 .bind(data.method.to_string())
488 .bind(data.uri.to_string())
489 .bind(headers_json)
490 .bind(body_json)
491 .bind(parsed)
492 .bind(&data.trace_id)
493 .bind(&data.span_id)
494 .execute(self.pool.write())
495 .await;
496 let query_duration = query_start.elapsed();
497 histogram!("outlet_write_duration_seconds", "operation" => "request")
498 .record(query_duration.as_secs_f64());
499
500 if let Err(e) = result {
501 counter!("outlet_write_errors_total", "operation" => "request").increment(1);
502 error!(correlation_id = %data.correlation_id, error = %e, "Failed to insert request data");
503 } else {
504 let processing_lag_ms = SystemTime::now()
505 .duration_since(data.timestamp)
506 .unwrap_or_default()
507 .as_millis();
508 if processing_lag_ms > 1000 {
509 warn!(correlation_id = %data.correlation_id, method = %data.method, uri = %data.uri, lag_ms = %processing_lag_ms, "Request logged (slow)");
510 } else {
511 debug!(correlation_id = %data.correlation_id, method = %data.method, uri = %data.uri, lag_ms = %processing_lag_ms, "Request logged");
512 }
513 }
514 }
515
516 #[instrument(name = "outlet.handle_response", skip(self, request_data, response_data), fields(correlation_id = %request_data.correlation_id))]
517 async fn handle_response(&self, request_data: RequestData, response_data: ResponseData) {
518 let headers_json = Self::headers_to_json(&response_data.headers);
519 let (body_json, parsed) = if response_data.body.is_some() {
520 let (json, parsed) =
521 self.response_body_to_json_with_fallback(&request_data, &response_data);
522 (Some(json), parsed)
523 } else {
524 (None, false)
525 };
526
527 let timestamp: DateTime<Utc> = response_data.timestamp.into();
528 let duration_ms = response_data.duration.as_millis() as i64;
529 let duration_to_first_byte_ms = response_data.duration_to_first_byte.as_millis() as i64;
530
531 let query_start = Instant::now();
532 let result = sqlx::query(
533 r#"
534 INSERT INTO http_responses (instance_id, correlation_id, timestamp, status_code, headers, body, body_parsed, duration_to_first_byte_ms, duration_ms)
535 SELECT $1, $2, $3, $4, $5, $6, $7, $8, $9
536 WHERE EXISTS (SELECT 1 FROM http_requests WHERE instance_id = $1 AND correlation_id = $2)
537 "#,
538 )
539 .bind(self.instance_id)
540 .bind(request_data.correlation_id as i64)
541 .bind(timestamp)
542 .bind(response_data.status.as_u16() as i32)
543 .bind(headers_json)
544 .bind(body_json)
545 .bind(parsed)
546 .bind(duration_to_first_byte_ms)
547 .bind(duration_ms)
548 .execute(self.pool.write())
549 .await;
550 let query_duration = query_start.elapsed();
551 histogram!("outlet_write_duration_seconds", "operation" => "response")
552 .record(query_duration.as_secs_f64());
553
554 match result {
555 Err(e) => {
556 counter!("outlet_write_errors_total", "operation" => "response").increment(1);
557 error!(correlation_id = %request_data.correlation_id, error = %e, "Failed to insert response data");
558 }
559 Ok(query_result) => {
560 if query_result.rows_affected() > 0 {
561 let processing_lag_ms = SystemTime::now()
562 .duration_since(response_data.timestamp)
563 .unwrap_or_default()
564 .as_millis();
565 if processing_lag_ms > 1000 {
566 warn!(correlation_id = %request_data.correlation_id, status = %response_data.status, duration_ms = %duration_ms, lag_ms = %processing_lag_ms, "Response logged (slow)");
567 } else {
568 debug!(correlation_id = %request_data.correlation_id, status = %response_data.status, duration_ms = %duration_ms, lag_ms = %processing_lag_ms, "Response logged");
569 }
570 } else {
571 debug!(correlation_id = %request_data.correlation_id, "No matching request found for response, skipping insert")
572 }
573 }
574 }
575 }
576
577 #[instrument(name = "outlet.handle_request_batch", skip(self, batch), fields(batch_size = batch.len()))]
578 async fn handle_request_batch(&self, batch: &[RequestData]) {
579 if batch.is_empty() {
580 return;
581 }
582
583 let len = batch.len();
584 let mut instance_ids = Vec::with_capacity(len);
585 let mut correlation_ids = Vec::with_capacity(len);
586 let mut timestamps = Vec::with_capacity(len);
587 let mut methods = Vec::with_capacity(len);
588 let mut uris = Vec::with_capacity(len);
589 let mut headers_col: Vec<Value> = Vec::with_capacity(len);
590 let mut bodies: Vec<Option<Value>> = Vec::with_capacity(len);
591 let mut body_parsed_col = Vec::with_capacity(len);
592 let mut trace_ids: Vec<Option<String>> = Vec::with_capacity(len);
593 let mut span_ids: Vec<Option<String>> = Vec::with_capacity(len);
594
595 for data in batch {
596 instance_ids.push(self.instance_id);
597 correlation_ids.push(data.correlation_id as i64);
598 timestamps.push(DateTime::<Utc>::from(data.timestamp));
599 methods.push(data.method.to_string());
600 uris.push(data.uri.to_string());
601 headers_col.push(Self::headers_to_json(&data.headers));
602
603 let (body_json, parsed) = if data.body.is_some() {
604 let (json, parsed) = self.request_body_to_json_with_fallback(data);
605 (Some(json), parsed)
606 } else {
607 (None, false)
608 };
609 bodies.push(body_json);
610 body_parsed_col.push(parsed);
611 trace_ids.push(data.trace_id.clone());
612 span_ids.push(data.span_id.clone());
613 }
614
615 let query_start = Instant::now();
616 let result = sqlx::query(
617 r#"
618 INSERT INTO http_requests (instance_id, correlation_id, timestamp, method, uri, headers, body, body_parsed, trace_id, span_id)
619 SELECT * FROM UNNEST($1::uuid[], $2::bigint[], $3::timestamptz[], $4::varchar[], $5::text[], $6::jsonb[], $7::jsonb[], $8::boolean[], $9::varchar[], $10::varchar[])
620 "#,
621 )
622 .bind(&instance_ids)
623 .bind(&correlation_ids)
624 .bind(×tamps)
625 .bind(&methods)
626 .bind(&uris)
627 .bind(&headers_col)
628 .bind(&bodies)
629 .bind(&body_parsed_col)
630 .bind(&trace_ids)
631 .bind(&span_ids)
632 .execute(self.pool.write())
633 .await;
634 let query_duration = query_start.elapsed();
635 histogram!("outlet_write_duration_seconds", "operation" => "request_batch")
636 .record(query_duration.as_secs_f64());
637
638 match result {
639 Ok(r) => {
640 debug!(
641 rows = r.rows_affected(),
642 duration_ms = query_duration.as_millis() as u64,
643 "Request batch inserted"
644 );
645 }
646 Err(e) => {
647 counter!("outlet_write_errors_total", "operation" => "request_batch").increment(1);
648 error!(batch_size = len, error = %e, "Failed to bulk insert request batch");
649 }
650 }
651 }
652
653 #[instrument(name = "outlet.handle_response_batch", skip(self, batch), fields(batch_size = batch.len()))]
654 async fn handle_response_batch(&self, batch: &[(RequestData, ResponseData)]) {
655 if batch.is_empty() {
656 return;
657 }
658
659 let len = batch.len();
660 let mut instance_ids = Vec::with_capacity(len);
661 let mut correlation_ids = Vec::with_capacity(len);
662 let mut timestamps = Vec::with_capacity(len);
663 let mut status_codes = Vec::with_capacity(len);
664 let mut headers_col: Vec<Value> = Vec::with_capacity(len);
665 let mut bodies: Vec<Option<Value>> = Vec::with_capacity(len);
666 let mut body_parsed_col = Vec::with_capacity(len);
667 let mut duration_to_first_byte_ms_col = Vec::with_capacity(len);
668 let mut duration_ms_col = Vec::with_capacity(len);
669
670 for (request_data, response_data) in batch {
671 instance_ids.push(self.instance_id);
672 correlation_ids.push(request_data.correlation_id as i64);
673 timestamps.push(DateTime::<Utc>::from(response_data.timestamp));
674 status_codes.push(response_data.status.as_u16() as i32);
675 headers_col.push(Self::headers_to_json(&response_data.headers));
676
677 let (body_json, parsed) = if response_data.body.is_some() {
678 let (json, parsed) =
679 self.response_body_to_json_with_fallback(request_data, response_data);
680 (Some(json), parsed)
681 } else {
682 (None, false)
683 };
684 bodies.push(body_json);
685 body_parsed_col.push(parsed);
686 duration_to_first_byte_ms_col
687 .push(response_data.duration_to_first_byte.as_millis() as i64);
688 duration_ms_col.push(response_data.duration.as_millis() as i64);
689 }
690
691 let query_start = Instant::now();
692 let result = sqlx::query(
693 r#"
694 INSERT INTO http_responses (instance_id, correlation_id, timestamp, status_code, headers, body, body_parsed, duration_to_first_byte_ms, duration_ms)
695 SELECT * FROM UNNEST($1::uuid[], $2::bigint[], $3::timestamptz[], $4::int[], $5::jsonb[], $6::jsonb[], $7::boolean[], $8::bigint[], $9::bigint[])
696 "#,
697 )
698 .bind(&instance_ids)
699 .bind(&correlation_ids)
700 .bind(×tamps)
701 .bind(&status_codes)
702 .bind(&headers_col)
703 .bind(&bodies)
704 .bind(&body_parsed_col)
705 .bind(&duration_to_first_byte_ms_col)
706 .bind(&duration_ms_col)
707 .execute(self.pool.write())
708 .await;
709 let query_duration = query_start.elapsed();
710 histogram!("outlet_write_duration_seconds", "operation" => "response_batch")
711 .record(query_duration.as_secs_f64());
712
713 match result {
714 Ok(r) => {
715 debug!(
716 rows = r.rows_affected(),
717 duration_ms = query_duration.as_millis() as u64,
718 "Response batch inserted"
719 );
720 }
721 Err(e) => {
722 counter!("outlet_write_errors_total", "operation" => "response_batch").increment(1);
723 error!(batch_size = len, error = %e, "Failed to bulk insert response batch");
724 }
725 }
726 }
727}
728
729#[cfg(test)]
730mod tests {
731 use super::*;
732 use bytes::Bytes;
733 use chrono::{DateTime, Utc};
734 use outlet::{RequestData, ResponseData};
735 use serde::{Deserialize, Serialize};
736 use serde_json::Value;
737 use sqlx::PgPool;
738 use std::collections::HashMap;
739 use std::time::{Duration, SystemTime};
740
741 #[derive(Debug, Clone, PartialEq, Deserialize, Serialize)]
742 struct TestRequest {
743 user_id: u64,
744 action: String,
745 }
746
747 #[derive(Debug, Clone, PartialEq, Deserialize, Serialize)]
748 struct TestResponse {
749 success: bool,
750 message: String,
751 }
752
753 fn create_test_request_data() -> RequestData {
754 let mut headers = HashMap::new();
755 headers.insert("content-type".to_string(), vec!["application/json".into()]);
756 headers.insert("user-agent".to_string(), vec!["test-client/1.0".into()]);
757
758 let test_req = TestRequest {
759 user_id: 123,
760 action: "create_user".to_string(),
761 };
762 let body = serde_json::to_vec(&test_req).unwrap();
763
764 RequestData {
765 method: http::Method::POST,
766 uri: http::Uri::from_static("/api/users"),
767 headers,
768 body: Some(Bytes::from(body)),
769 timestamp: SystemTime::now(),
770 correlation_id: 0,
771 trace_id: None,
772 span_id: None,
773 }
774 }
775
776 fn create_test_response_data() -> ResponseData {
777 let mut headers = HashMap::new();
778 headers.insert("content-type".to_string(), vec!["application/json".into()]);
779
780 let test_res = TestResponse {
781 success: true,
782 message: "User created successfully".to_string(),
783 };
784 let body = serde_json::to_vec(&test_res).unwrap();
785
786 ResponseData {
787 status: http::StatusCode::CREATED,
788 headers,
789 body: Some(Bytes::from(body)),
790 timestamp: SystemTime::now(),
791 duration_to_first_byte: Duration::from_millis(100),
792 duration: Duration::from_millis(150),
793 correlation_id: 0,
794 extensions: Default::default(),
795 }
796 }
797
798 #[sqlx::test]
799 async fn test_handler_creation(pool: PgPool) {
800 crate::migrator().run(&pool).await.unwrap();
802
803 let handler = PostgresHandler::<PgPool, TestRequest, TestResponse>::from_pool(pool.clone())
804 .await
805 .unwrap();
806
807 let repository = handler.repository();
809
810 let filter = RequestFilter::default();
812 let results = repository.query(filter).await.unwrap();
813 assert!(results.is_empty());
814 }
815
816 #[sqlx::test]
817 async fn test_handle_request_with_typed_body(pool: PgPool) {
818 crate::migrator().run(&pool).await.unwrap();
820
821 let handler = PostgresHandler::<PgPool, TestRequest, TestResponse>::from_pool(pool.clone())
822 .await
823 .unwrap();
824 let repository = handler.repository();
825
826 let mut request_data = create_test_request_data();
827 let correlation_id = 12345;
828 request_data.correlation_id = correlation_id;
829
830 handler.handle_request(request_data.clone()).await;
832
833 let filter = RequestFilter {
835 correlation_id: Some(correlation_id as i64),
836 ..Default::default()
837 };
838 let results = repository.query(filter).await.unwrap();
839
840 assert_eq!(results.len(), 1);
841 let pair = &results[0];
842
843 assert_eq!(pair.request.correlation_id, correlation_id as i64);
844 assert_eq!(pair.request.method, "POST");
845 assert_eq!(pair.request.uri, "/api/users");
846
847 match &pair.request.body {
849 Some(Ok(parsed_body)) => {
850 assert_eq!(
851 *parsed_body,
852 TestRequest {
853 user_id: 123,
854 action: "create_user".to_string(),
855 }
856 );
857 }
858 _ => panic!("Expected successfully parsed request body"),
859 }
860
861 let headers_value = &pair.request.headers;
863 assert!(headers_value.get("content-type").is_some());
864 assert!(headers_value.get("user-agent").is_some());
865
866 assert!(pair.response.is_none());
868 }
869
870 #[sqlx::test]
871 async fn test_handle_response_with_typed_body(pool: PgPool) {
872 crate::migrator().run(&pool).await.unwrap();
874
875 let handler = PostgresHandler::<PgPool, TestRequest, TestResponse>::from_pool(pool.clone())
876 .await
877 .unwrap();
878 let repository = handler.repository();
879
880 let mut request_data = create_test_request_data();
881 let mut response_data = create_test_response_data();
882 let correlation_id = 54321;
883 request_data.correlation_id = correlation_id;
884 response_data.correlation_id = correlation_id;
885
886 handler.handle_request(request_data.clone()).await;
888 handler
889 .handle_response(request_data, response_data.clone())
890 .await;
891
892 let filter = RequestFilter {
894 correlation_id: Some(correlation_id as i64),
895 ..Default::default()
896 };
897 let results = repository.query(filter).await.unwrap();
898
899 assert_eq!(results.len(), 1);
900 let pair = &results[0];
901
902 let response = pair.response.as_ref().expect("Response should be present");
904 assert_eq!(response.correlation_id, correlation_id as i64);
905 assert_eq!(response.status_code, 201);
906 assert_eq!(response.duration_ms, 150);
907
908 match &response.body {
910 Some(Ok(parsed_body)) => {
911 assert_eq!(
912 *parsed_body,
913 TestResponse {
914 success: true,
915 message: "User created successfully".to_string(),
916 }
917 );
918 }
919 _ => panic!("Expected successfully parsed response body"),
920 }
921 }
922
923 #[sqlx::test]
924 async fn test_handle_unparseable_body_fallback(pool: PgPool) {
925 crate::migrator().run(&pool).await.unwrap();
927
928 let handler = PostgresHandler::<PgPool, TestRequest, TestResponse>::from_pool(pool.clone())
929 .await
930 .unwrap();
931 let repository = handler.repository();
932
933 let mut headers = HashMap::new();
935 headers.insert("content-type".to_string(), vec!["text/plain".into()]);
936
937 let invalid_json_body = b"not valid json for TestRequest";
938 let correlation_id = 99999;
939 let request_data = RequestData {
940 method: http::Method::POST,
941 uri: http::Uri::from_static("/api/test"),
942 headers,
943 body: Some(Bytes::from(invalid_json_body.to_vec())),
944 timestamp: SystemTime::now(),
945 correlation_id,
946 trace_id: None,
947 span_id: None,
948 };
949
950 handler.handle_request(request_data).await;
951
952 let filter = RequestFilter {
954 correlation_id: Some(correlation_id as i64),
955 ..Default::default()
956 };
957 let results = repository.query(filter).await.unwrap();
958
959 assert_eq!(results.len(), 1);
960 let pair = &results[0];
961
962 match &pair.request.body {
964 Some(Err(raw_bytes)) => {
965 assert_eq!(raw_bytes.as_ref(), invalid_json_body);
966 }
967 _ => panic!("Expected raw bytes fallback for unparseable body"),
968 }
969 }
970
971 #[sqlx::test]
972 async fn test_query_with_multiple_filters(pool: PgPool) {
973 crate::migrator().run(&pool).await.unwrap();
975
976 let handler = PostgresHandler::<PgPool, Value, Value>::from_pool(pool.clone())
977 .await
978 .unwrap();
979 let repository = handler.repository();
980
981 let test_cases = vec![
983 (1001, "GET", "/api/users", 200, 100),
984 (1002, "POST", "/api/users", 201, 150),
985 (1003, "GET", "/api/orders", 404, 50),
986 (1004, "PUT", "/api/users/123", 200, 300),
987 ];
988
989 for (correlation_id, method, uri, status, duration_ms) in test_cases {
990 let mut headers = HashMap::new();
991 headers.insert("content-type".to_string(), vec!["application/json".into()]);
992
993 let request_data = RequestData {
994 method: method.parse().unwrap(),
995 uri: uri.parse().unwrap(),
996 headers: headers.clone(),
997 body: Some(Bytes::from(b"{}".to_vec())),
998 timestamp: SystemTime::now(),
999 correlation_id,
1000 trace_id: None,
1001 span_id: None,
1002 };
1003
1004 let response_data = ResponseData {
1005 correlation_id,
1006 status: http::StatusCode::from_u16(status).unwrap(),
1007 headers,
1008 body: Some(Bytes::from(b"{}".to_vec())),
1009 timestamp: SystemTime::now(),
1010 duration_to_first_byte: Duration::from_millis(duration_ms / 2),
1011 duration: Duration::from_millis(duration_ms),
1012 extensions: Default::default(),
1013 };
1014
1015 handler.handle_request(request_data.clone()).await;
1016 handler.handle_response(request_data, response_data).await;
1017 }
1018
1019 let filter = RequestFilter {
1021 method: Some("GET".to_string()),
1022 ..Default::default()
1023 };
1024 let results = repository.query(filter).await.unwrap();
1025 assert_eq!(results.len(), 2); let filter = RequestFilter {
1029 status_code: Some(200),
1030 ..Default::default()
1031 };
1032 let results = repository.query(filter).await.unwrap();
1033 assert_eq!(results.len(), 2); let filter = RequestFilter {
1037 uri_pattern: Some("/api/users%".to_string()),
1038 ..Default::default()
1039 };
1040 let results = repository.query(filter).await.unwrap();
1041 assert_eq!(results.len(), 3); let filter = RequestFilter {
1045 min_duration_ms: Some(100),
1046 max_duration_ms: Some(200),
1047 ..Default::default()
1048 };
1049 let results = repository.query(filter).await.unwrap();
1050 assert_eq!(results.len(), 2); let filter = RequestFilter {
1054 method: Some("GET".to_string()),
1055 status_code: Some(200),
1056 ..Default::default()
1057 };
1058 let results = repository.query(filter).await.unwrap();
1059 assert_eq!(results.len(), 1); assert_eq!(results[0].request.correlation_id, 1001);
1061 }
1062
1063 #[sqlx::test]
1064 async fn test_query_with_pagination_and_ordering(pool: PgPool) {
1065 crate::migrator().run(&pool).await.unwrap();
1067
1068 let handler = PostgresHandler::<PgPool, Value, Value>::from_pool(pool.clone())
1069 .await
1070 .unwrap();
1071 let repository = handler.repository();
1072
1073 let now = SystemTime::now();
1075 for i in 0..5 {
1076 let correlation_id = 2000 + i;
1077 let timestamp = now + Duration::from_secs(i * 10); let mut headers = HashMap::new();
1080 headers.insert("x-test-id".to_string(), vec![i.to_string().into()]);
1081
1082 let request_data = RequestData {
1083 method: http::Method::GET,
1084 uri: "/api/test".parse().unwrap(),
1085 headers,
1086 body: Some(Bytes::from(format!("{{\"id\": {i}}}").into_bytes())),
1087 timestamp,
1088 correlation_id,
1089 trace_id: None,
1090 span_id: None,
1091 };
1092
1093 handler.handle_request(request_data).await;
1094 }
1095
1096 let filter = RequestFilter {
1098 limit: Some(3),
1099 ..Default::default()
1100 };
1101 let results = repository.query(filter).await.unwrap();
1102 assert_eq!(results.len(), 3);
1103
1104 for i in 0..2 {
1106 assert!(results[i].request.timestamp <= results[i + 1].request.timestamp);
1107 }
1108
1109 let filter = RequestFilter {
1111 order_by_timestamp_desc: true,
1112 limit: Some(2),
1113 offset: Some(1),
1114 ..Default::default()
1115 };
1116 let results = repository.query(filter).await.unwrap();
1117 assert_eq!(results.len(), 2);
1118
1119 assert!(results[0].request.timestamp >= results[1].request.timestamp);
1121 }
1122
1123 #[sqlx::test]
1124 async fn test_headers_conversion(pool: PgPool) {
1125 crate::migrator().run(&pool).await.unwrap();
1127
1128 let handler = PostgresHandler::<PgPool, Value, Value>::from_pool(pool.clone())
1129 .await
1130 .unwrap();
1131 let repository = handler.repository();
1132
1133 let mut headers = HashMap::new();
1135 headers.insert("single-value".to_string(), vec!["test".into()]);
1136 headers.insert(
1137 "multi-value".to_string(),
1138 vec!["val1".into(), "val2".into()],
1139 );
1140 headers.insert("empty-value".to_string(), vec!["".into()]);
1141
1142 let request_data = RequestData {
1143 correlation_id: 3000,
1144 method: http::Method::GET,
1145 uri: "/test".parse().unwrap(),
1146 headers,
1147 body: None,
1148 timestamp: SystemTime::now(),
1149 trace_id: None,
1150 span_id: None,
1151 };
1152
1153 let correlation_id = 3000;
1154 handler.handle_request(request_data).await;
1155
1156 let filter = RequestFilter {
1157 correlation_id: Some(correlation_id as i64),
1158 ..Default::default()
1159 };
1160 let results = repository.query(filter).await.unwrap();
1161
1162 assert_eq!(results.len(), 1);
1163 let headers_json = &results[0].request.headers;
1164
1165 assert_eq!(
1167 headers_json["single-value"],
1168 Value::String("test".to_string())
1169 );
1170
1171 match &headers_json["multi-value"] {
1173 Value::Array(arr) => {
1174 assert_eq!(arr.len(), 2);
1175 assert_eq!(arr[0], Value::String("val1".to_string()));
1176 assert_eq!(arr[1], Value::String("val2".to_string()));
1177 }
1178 _ => panic!("Expected array for multi-value header"),
1179 }
1180
1181 assert_eq!(headers_json["empty-value"], Value::String("".to_string()));
1183 }
1184
1185 #[sqlx::test]
1186 async fn test_timestamp_filtering(pool: PgPool) {
1187 crate::migrator().run(&pool).await.unwrap();
1189
1190 let handler = PostgresHandler::<PgPool, Value, Value>::from_pool(pool.clone())
1191 .await
1192 .unwrap();
1193 let repository = handler.repository();
1194
1195 let base_time = SystemTime::UNIX_EPOCH + Duration::from_secs(1_600_000_000); let times = [
1199 base_time + Duration::from_secs(0), base_time + Duration::from_secs(3600), base_time + Duration::from_secs(7200), ];
1203
1204 for (i, timestamp) in times.iter().enumerate() {
1205 let correlation_id = 4001 + i as u64;
1206 let request_data = RequestData {
1207 method: http::Method::GET,
1208 uri: "/test".parse().unwrap(),
1209 headers: HashMap::new(),
1210 body: None,
1211 timestamp: *timestamp,
1212 correlation_id,
1213 trace_id: None,
1214 span_id: None,
1215 };
1216
1217 handler.handle_request(request_data).await;
1218 }
1219
1220 let after_time: DateTime<Utc> = (base_time + Duration::from_secs(1800)).into(); let filter = RequestFilter {
1223 timestamp_after: Some(after_time),
1224 ..Default::default()
1225 };
1226 let results = repository.query(filter).await.unwrap();
1227 assert_eq!(results.len(), 2); let before_time: DateTime<Utc> = (base_time + Duration::from_secs(5400)).into(); let filter = RequestFilter {
1232 timestamp_before: Some(before_time),
1233 ..Default::default()
1234 };
1235 let results = repository.query(filter).await.unwrap();
1236 assert_eq!(results.len(), 2); let filter = RequestFilter {
1240 timestamp_after: Some(after_time),
1241 timestamp_before: Some(before_time),
1242 ..Default::default()
1243 };
1244 let results = repository.query(filter).await.unwrap();
1245 assert_eq!(results.len(), 1); assert_eq!(results[0].request.correlation_id, 4002);
1247 }
1248
1249 #[sqlx::test]
1254 async fn test_no_path_filtering_logs_everything(pool: PgPool) {
1255 crate::migrator().run(&pool).await.unwrap();
1257
1258 let handler = PostgresHandler::<PgPool, Value, Value>::from_pool(pool.clone())
1260 .await
1261 .unwrap();
1262 let repository = handler.repository();
1263
1264 let test_uris = ["/api/users", "/health", "/metrics", "/random/path"];
1265 for (i, uri) in test_uris.iter().enumerate() {
1266 let correlation_id = 3000 + i as u64;
1267 let mut headers = HashMap::new();
1268 headers.insert("content-type".to_string(), vec!["application/json".into()]);
1269
1270 let request_data = RequestData {
1271 method: http::Method::GET,
1272 uri: uri.parse().unwrap(),
1273 headers,
1274 body: Some(Bytes::from(b"{}".to_vec())),
1275 timestamp: SystemTime::now(),
1276 correlation_id,
1277 trace_id: None,
1278 span_id: None,
1279 };
1280
1281 handler.handle_request(request_data).await;
1282 }
1283
1284 let filter = RequestFilter::default();
1286 let results = repository.query(filter).await.unwrap();
1287 assert_eq!(results.len(), 4);
1288 }
1289
1290 #[sqlx::test]
1292 async fn test_write_operations_use_write_pool(pool: PgPool) {
1293 crate::migrator().run(&pool).await.unwrap();
1295
1296 let test_pools = crate::TestDbPools::new(pool).await.unwrap();
1298 let handler = PostgresHandler::<_, Value, Value>::from_pool_provider(test_pools.clone())
1299 .await
1300 .unwrap();
1301
1302 let mut request_data = create_test_request_data();
1303 let correlation_id = 5001;
1304 request_data.correlation_id = correlation_id;
1305
1306 handler.handle_request(request_data.clone()).await;
1308
1309 let count: i64 =
1311 sqlx::query_scalar("SELECT COUNT(*) FROM http_requests WHERE correlation_id = $1")
1312 .bind(correlation_id as i64)
1313 .fetch_one(test_pools.write())
1314 .await
1315 .unwrap();
1316
1317 assert_eq!(count, 1, "Request should be written to primary pool");
1318 }
1319
1320 #[sqlx::test]
1321 async fn test_response_write_uses_write_pool(pool: PgPool) {
1322 crate::migrator().run(&pool).await.unwrap();
1324
1325 let test_pools = crate::TestDbPools::new(pool).await.unwrap();
1326 let handler = PostgresHandler::<_, Value, Value>::from_pool_provider(test_pools.clone())
1327 .await
1328 .unwrap();
1329
1330 let mut request_data = create_test_request_data();
1331 let mut response_data = create_test_response_data();
1332 let correlation_id = 5002;
1333 request_data.correlation_id = correlation_id;
1334 response_data.correlation_id = correlation_id;
1335
1336 handler.handle_request(request_data.clone()).await;
1338
1339 handler.handle_response(request_data, response_data).await;
1341
1342 let count: i64 =
1344 sqlx::query_scalar("SELECT COUNT(*) FROM http_responses WHERE correlation_id = $1")
1345 .bind(correlation_id as i64)
1346 .fetch_one(test_pools.write())
1347 .await
1348 .unwrap();
1349
1350 assert_eq!(count, 1, "Response should be written to primary pool");
1351 }
1352
1353 #[sqlx::test]
1354 async fn test_repository_queries_use_read_pool(pool: PgPool) {
1355 crate::migrator().run(&pool).await.unwrap();
1357
1358 let test_pools = crate::TestDbPools::new(pool).await.unwrap();
1359 let handler = PostgresHandler::<_, Value, Value>::from_pool_provider(test_pools.clone())
1360 .await
1361 .unwrap();
1362
1363 let mut request_data = create_test_request_data();
1365 let correlation_id = 5003;
1366 request_data.correlation_id = correlation_id;
1367 handler.handle_request(request_data).await;
1368
1369 let repository = handler.repository();
1371 let filter = RequestFilter {
1372 correlation_id: Some(correlation_id as i64),
1373 ..Default::default()
1374 };
1375
1376 let results = repository.query(filter).await.unwrap();
1378 assert_eq!(results.len(), 1);
1379 assert_eq!(results[0].request.correlation_id, correlation_id as i64);
1380 }
1381
1382 #[sqlx::test]
1383 async fn test_replica_pool_rejects_writes(pool: PgPool) {
1384 crate::migrator().run(&pool).await.unwrap();
1386
1387 let test_pools = crate::TestDbPools::new(pool).await.unwrap();
1388
1389 let result = sqlx::query("INSERT INTO http_requests (instance_id, correlation_id, timestamp, method, uri, headers, body, body_parsed) VALUES ($1, $2, $3, $4, $5, $6, $7, $8)")
1391 .bind(Uuid::new_v4())
1392 .bind(9999i64)
1393 .bind(Utc::now())
1394 .bind("GET")
1395 .bind("/test")
1396 .bind(serde_json::json!({}))
1397 .bind(None::<Value>)
1398 .bind(false)
1399 .execute(test_pools.read())
1400 .await;
1401
1402 assert!(
1404 result.is_err(),
1405 "Replica pool should reject write operations"
1406 );
1407
1408 let err = result.unwrap_err();
1409 let err_msg = err.to_string().to_lowercase();
1410 assert!(
1411 err_msg.contains("read-only") || err_msg.contains("read only"),
1412 "Error should mention read-only: {}",
1413 err
1414 );
1415 }
1416
1417 #[sqlx::test]
1418 async fn test_full_request_response_cycle_with_read_write_separation(pool: PgPool) {
1419 crate::migrator().run(&pool).await.unwrap();
1421
1422 let test_pools = crate::TestDbPools::new(pool).await.unwrap();
1423 let handler =
1424 PostgresHandler::<_, TestRequest, TestResponse>::from_pool_provider(test_pools)
1425 .await
1426 .unwrap();
1427
1428 let mut request_data = create_test_request_data();
1429 let mut response_data = create_test_response_data();
1430 let correlation_id = 5004;
1431 request_data.correlation_id = correlation_id;
1432 response_data.correlation_id = correlation_id;
1433
1434 handler.handle_request(request_data.clone()).await;
1436 handler.handle_response(request_data, response_data).await;
1437
1438 let repository = handler.repository();
1440 let filter = RequestFilter {
1441 correlation_id: Some(correlation_id as i64),
1442 ..Default::default()
1443 };
1444
1445 let results = repository.query(filter).await.unwrap();
1446 assert_eq!(results.len(), 1);
1447
1448 let pair = &results[0];
1450 assert_eq!(pair.request.correlation_id, correlation_id as i64);
1451 assert_eq!(pair.request.method, "POST");
1452 assert_eq!(pair.request.uri, "/api/users");
1453
1454 let response = pair.response.as_ref().expect("Response should exist");
1456 assert_eq!(response.correlation_id, correlation_id as i64);
1457 assert_eq!(response.status_code, 201);
1458
1459 match &pair.request.body {
1461 Some(Ok(parsed_body)) => {
1462 assert_eq!(
1463 *parsed_body,
1464 TestRequest {
1465 user_id: 123,
1466 action: "create_user".to_string(),
1467 }
1468 );
1469 }
1470 _ => panic!("Expected successfully parsed request body"),
1471 }
1472
1473 match &response.body {
1474 Some(Ok(parsed_body)) => {
1475 assert_eq!(
1476 *parsed_body,
1477 TestResponse {
1478 success: true,
1479 message: "User created successfully".to_string(),
1480 }
1481 );
1482 }
1483 _ => panic!("Expected successfully parsed response body"),
1484 }
1485 }
1486
1487 #[sqlx::test]
1492 async fn test_request_batch_insert(pool: PgPool) {
1493 crate::migrator().run(&pool).await.unwrap();
1494 let handler = PostgresHandler::<PgPool, TestRequest, TestResponse>::from_pool(pool.clone())
1495 .await
1496 .unwrap();
1497
1498 let mut batch = Vec::new();
1499 for i in 0..5 {
1500 let mut req = create_test_request_data();
1501 req.correlation_id = 1000 + i;
1502 req.uri = format!("/api/batch/{i}").parse().unwrap();
1503 batch.push(req);
1504 }
1505
1506 handler.handle_request_batch(&batch).await;
1507
1508 let count: (i64,) = sqlx::query_as(
1510 "SELECT COUNT(*) FROM http_requests WHERE correlation_id BETWEEN 1000 AND 1004",
1511 )
1512 .fetch_one(&pool)
1513 .await
1514 .unwrap();
1515 assert_eq!(count.0, 5);
1516 }
1517
1518 #[sqlx::test]
1519 async fn test_response_batch_insert(pool: PgPool) {
1520 crate::migrator().run(&pool).await.unwrap();
1521 let handler = PostgresHandler::<PgPool, TestRequest, TestResponse>::from_pool(pool.clone())
1522 .await
1523 .unwrap();
1524
1525 let mut pairs = Vec::new();
1527 for i in 0..3 {
1528 let mut req = create_test_request_data();
1529 req.correlation_id = 2000 + i;
1530 handler.handle_request(req.clone()).await;
1531
1532 let mut res = create_test_response_data();
1533 res.correlation_id = 2000 + i;
1534 pairs.push((req, res));
1535 }
1536
1537 handler.handle_response_batch(&pairs).await;
1538
1539 let count: (i64,) = sqlx::query_as(
1541 "SELECT COUNT(*) FROM http_responses WHERE correlation_id BETWEEN 2000 AND 2002",
1542 )
1543 .fetch_one(&pool)
1544 .await
1545 .unwrap();
1546 assert_eq!(count.0, 3);
1547 }
1548
1549 #[sqlx::test]
1550 async fn test_batch_with_mixed_bodies(pool: PgPool) {
1551 crate::migrator().run(&pool).await.unwrap();
1552 let handler = PostgresHandler::<PgPool, TestRequest, TestResponse>::from_pool(pool.clone())
1553 .await
1554 .unwrap();
1555
1556 let mut batch = Vec::new();
1557
1558 let mut req_with_body = create_test_request_data();
1560 req_with_body.correlation_id = 3000;
1561 batch.push(req_with_body);
1562
1563 let mut req_no_body = create_test_request_data();
1565 req_no_body.correlation_id = 3001;
1566 req_no_body.body = None;
1567 batch.push(req_no_body);
1568
1569 let mut req_bad_body = create_test_request_data();
1571 req_bad_body.correlation_id = 3002;
1572 req_bad_body.body = Some(Bytes::from("not valid json"));
1573 batch.push(req_bad_body);
1574
1575 handler.handle_request_batch(&batch).await;
1576
1577 let count: (i64,) = sqlx::query_as(
1579 "SELECT COUNT(*) FROM http_requests WHERE correlation_id BETWEEN 3000 AND 3002",
1580 )
1581 .fetch_one(&pool)
1582 .await
1583 .unwrap();
1584 assert_eq!(count.0, 3);
1585
1586 let rows: Vec<(i64, Option<bool>)> = sqlx::query_as(
1588 "SELECT correlation_id, body_parsed FROM http_requests WHERE correlation_id BETWEEN 3000 AND 3002 ORDER BY correlation_id",
1589 )
1590 .fetch_all(&pool)
1591 .await
1592 .unwrap();
1593
1594 assert_eq!(rows[0].1, Some(true)); assert_eq!(rows[1].1, Some(false)); assert_eq!(rows[2].1, Some(false)); }
1598
1599 #[sqlx::test]
1600 async fn test_empty_batch_is_noop(pool: PgPool) {
1601 crate::migrator().run(&pool).await.unwrap();
1602 let handler = PostgresHandler::<PgPool, TestRequest, TestResponse>::from_pool(pool.clone())
1603 .await
1604 .unwrap();
1605
1606 handler.handle_request_batch(&[]).await;
1608 handler.handle_response_batch(&[]).await;
1609 }
1610
1611 #[sqlx::test]
1612 async fn test_batch_write_uses_write_pool(pool: PgPool) {
1613 use sqlx_pool_router::TestDbPools;
1614 crate::migrator().run(&pool).await.unwrap();
1615 let test_pools = TestDbPools::new(pool).await.unwrap();
1616 let handler =
1617 PostgresHandler::<TestDbPools, TestRequest, TestResponse>::from_pool_provider(
1618 test_pools,
1619 )
1620 .await
1621 .unwrap();
1622
1623 let mut req = create_test_request_data();
1624 req.correlation_id = 4000;
1625 handler.handle_request_batch(&[req.clone()]).await;
1626
1627 let res = create_test_response_data();
1628 handler.handle_response_batch(&[(req, res)]).await;
1629 }
1630}