1use anyhow::{anyhow, Result};
15use serde::{Deserialize, Serialize};
16use std::collections::HashMap;
17use std::fmt;
18use std::sync::Arc;
19use std::time::{Duration, Instant, SystemTime};
20use tokio::sync::RwLock;
21use tracing::{info, warn};
22
23use scirs2_core::random::secure::SecureRandom;
25
26use sha2::Sha256;
28
29#[derive(Debug, Clone, Serialize, Deserialize)]
35pub struct CorsConfig {
36 pub allowed_origins: Vec<String>,
39
40 pub allowed_methods: Vec<String>,
42
43 pub allowed_headers: Vec<String>,
45
46 pub exposed_headers: Vec<String>,
48
49 pub allow_credentials: bool,
51
52 pub max_age: Option<u64>,
54
55 pub wildcard_origins: bool,
57}
58
59impl Default for CorsConfig {
60 fn default() -> Self {
61 Self {
62 allowed_origins: vec!["*".to_string()],
63 allowed_methods: vec!["GET".to_string(), "POST".to_string(), "OPTIONS".to_string()],
64 allowed_headers: vec![
65 "Content-Type".to_string(),
66 "Authorization".to_string(),
67 "X-Requested-With".to_string(),
68 ],
69 exposed_headers: vec!["Content-Length".to_string()],
70 allow_credentials: false,
71 max_age: Some(3600), wildcard_origins: false,
73 }
74 }
75}
76
77impl CorsConfig {
78 pub fn production(origins: Vec<String>) -> Self {
80 Self {
81 allowed_origins: origins,
82 allowed_methods: vec!["GET".to_string(), "POST".to_string(), "OPTIONS".to_string()],
83 allowed_headers: vec![
84 "Content-Type".to_string(),
85 "Authorization".to_string(),
86 "X-Request-ID".to_string(),
87 "X-API-Key".to_string(),
88 ],
89 exposed_headers: vec!["Content-Length".to_string(), "X-Request-ID".to_string()],
90 allow_credentials: true,
91 max_age: Some(86400), wildcard_origins: false,
93 }
94 }
95
96 pub fn is_origin_allowed(&self, origin: &str) -> bool {
98 if self.allowed_origins.contains(&"*".to_string()) {
99 return true;
100 }
101
102 if self.wildcard_origins {
103 for allowed in &self.allowed_origins {
104 if Self::wildcard_match(allowed, origin) {
105 return true;
106 }
107 }
108 }
109
110 self.allowed_origins.contains(&origin.to_string())
111 }
112
113 fn wildcard_match(pattern: &str, origin: &str) -> bool {
115 if pattern.contains('*') {
116 let parts: Vec<&str> = pattern.split('*').collect();
117 if parts.len() == 2 {
118 origin.starts_with(parts[0]) && origin.ends_with(parts[1])
119 } else {
120 false
121 }
122 } else {
123 pattern == origin
124 }
125 }
126}
127
128#[derive(Debug, Clone, Serialize, Deserialize)]
134pub struct JwtConfig {
135 pub secret: String,
137
138 pub algorithm: JwtAlgorithm,
140
141 pub expiration: u64,
143
144 pub issuer: Option<String>,
146
147 pub audience: Option<Vec<String>>,
149
150 pub required_claims: Vec<String>,
152
153 pub enable_refresh: bool,
155
156 pub refresh_expiration: u64,
158}
159
160impl Default for JwtConfig {
161 fn default() -> Self {
162 Self {
163 secret: "".to_string(), algorithm: JwtAlgorithm::HS256,
165 expiration: 3600, issuer: None,
167 audience: None,
168 required_claims: vec!["sub".to_string()],
169 enable_refresh: true,
170 refresh_expiration: 604800, }
172 }
173}
174
175#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
177pub enum JwtAlgorithm {
178 HS256,
180 HS384,
182 HS512,
184 RS256,
186 RS384,
188 RS512,
190}
191
192impl fmt::Display for JwtAlgorithm {
193 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
194 match self {
195 JwtAlgorithm::HS256 => write!(f, "HS256"),
196 JwtAlgorithm::HS384 => write!(f, "HS384"),
197 JwtAlgorithm::HS512 => write!(f, "HS512"),
198 JwtAlgorithm::RS256 => write!(f, "RS256"),
199 JwtAlgorithm::RS384 => write!(f, "RS384"),
200 JwtAlgorithm::RS512 => write!(f, "RS512"),
201 }
202 }
203}
204
205#[derive(Debug, Clone, Serialize, Deserialize)]
207pub struct JwtClaims {
208 pub sub: String,
210
211 pub exp: u64,
213
214 pub iat: u64,
216
217 #[serde(skip_serializing_if = "Option::is_none")]
219 pub iss: Option<String>,
220
221 #[serde(skip_serializing_if = "Option::is_none")]
223 pub aud: Option<Vec<String>>,
224
225 #[serde(flatten)]
227 pub custom: HashMap<String, serde_json::Value>,
228}
229
230impl JwtClaims {
231 pub fn new(sub: String, expiration: u64) -> Self {
233 let now = SystemTime::now()
234 .duration_since(SystemTime::UNIX_EPOCH)
235 .expect("SystemTime should be after UNIX_EPOCH")
236 .as_secs();
237
238 Self {
239 sub,
240 exp: now + expiration,
241 iat: now,
242 iss: None,
243 aud: None,
244 custom: HashMap::new(),
245 }
246 }
247
248 pub fn with_claim(mut self, key: String, value: serde_json::Value) -> Self {
250 self.custom.insert(key, value);
251 self
252 }
253
254 pub fn is_expired(&self) -> bool {
256 let now = SystemTime::now()
257 .duration_since(SystemTime::UNIX_EPOCH)
258 .expect("SystemTime should be after UNIX_EPOCH")
259 .as_secs();
260 now >= self.exp
261 }
262}
263
264pub struct JwtManager {
266 config: JwtConfig,
267}
268
269impl JwtManager {
270 pub fn new(config: JwtConfig) -> Result<Self> {
272 if config.secret.is_empty() {
273 return Err(anyhow!("JWT secret cannot be empty"));
274 }
275 Ok(Self { config })
276 }
277
278 pub fn generate_secure_secret() -> String {
293 let mut rng = SecureRandom::new();
294 rng.random_alphanumeric(64)
295 }
296
297 pub fn generate_token(&self, claims: &JwtClaims) -> Result<String> {
304 if self.config.secret.is_empty() {
305 return Err(anyhow!("JWT secret not configured"));
306 }
307
308 let header = serde_json::json!({
310 "typ": "JWT",
311 "alg": self.config.algorithm.to_string()
312 });
313
314 let header_b64 = base64_url_encode(&serde_json::to_string(&header)?);
316 let payload_b64 = base64_url_encode(&serde_json::to_string(claims)?);
317
318 let signing_input = format!("{}.{}", header_b64, payload_b64);
320
321 let signature = self.sign_hmac_sha256(&signing_input)?;
323 let signature_b64 = base64_url_encode(&hex::encode(&signature));
324
325 Ok(format!("{}.{}", signing_input, signature_b64))
327 }
328
329 fn sign_hmac_sha256(&self, data: &str) -> Result<Vec<u8>> {
331 use hmac::{Hmac, KeyInit, Mac};
332 type HmacSha256 = Hmac<Sha256>;
333
334 let mut mac = HmacSha256::new_from_slice(self.config.secret.as_bytes())
335 .map_err(|e| anyhow!("Invalid key length: {}", e))?;
336
337 mac.update(data.as_bytes());
338 Ok(mac.finalize().into_bytes().to_vec())
339 }
340
341 pub fn verify_token(&self, token: &str) -> Result<JwtClaims> {
351 let parts: Vec<&str> = token.split('.').collect();
353 if parts.len() != 3 {
354 return Err(anyhow!(
355 "Invalid token format: expected 3 parts, got {}",
356 parts.len()
357 ));
358 }
359
360 let signing_input = format!("{}.{}", parts[0], parts[1]);
362 let expected_signature = self.sign_hmac_sha256(&signing_input)?;
363 let expected_signature_b64 = base64_url_encode(&hex::encode(&expected_signature));
364
365 if parts[2] != expected_signature_b64 {
366 return Err(anyhow!("Invalid token signature"));
367 }
368
369 let payload_json = base64_url_decode(parts[1])?;
371 let claims: JwtClaims = serde_json::from_str(&payload_json)
372 .map_err(|e| anyhow!("Invalid token payload: {}", e))?;
373
374 if claims.is_expired() {
376 return Err(anyhow!("Token expired at {}", claims.exp));
377 }
378
379 Ok(claims)
380 }
381}
382
383fn base64_url_encode(data: &str) -> String {
388 const CHARSET: &[u8] = b"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-_";
391
392 let bytes = data.as_bytes();
393 let mut result = String::with_capacity((bytes.len() * 4 / 3) + 4);
394
395 for chunk in bytes.chunks(3) {
396 let b1 = chunk[0];
397 let b2 = chunk.get(1).copied().unwrap_or(0);
398 let b3 = chunk.get(2).copied().unwrap_or(0);
399
400 let n = (b1 as u32) << 16 | (b2 as u32) << 8 | (b3 as u32);
401
402 result.push(CHARSET[((n >> 18) & 0x3F) as usize] as char);
403 result.push(CHARSET[((n >> 12) & 0x3F) as usize] as char);
404
405 if chunk.len() > 1 {
406 result.push(CHARSET[((n >> 6) & 0x3F) as usize] as char);
407 }
408 if chunk.len() > 2 {
409 result.push(CHARSET[(n & 0x3F) as usize] as char);
410 }
411 }
412
413 result
415}
416
417fn base64_url_decode(data: &str) -> Result<String> {
419 let data = data.replace('-', "+").replace('_', "/");
422
423 let padding = match data.len() % 4 {
425 2 => "==",
426 3 => "=",
427 _ => "",
428 };
429 let data_with_padding = format!("{}{}", data, padding);
430
431 let decoded_bytes = decode_base64_standard(&data_with_padding)?;
433 String::from_utf8(decoded_bytes).map_err(|e| anyhow!("Invalid UTF-8 in decoded data: {}", e))
434}
435
436fn decode_base64_standard(data: &str) -> Result<Vec<u8>> {
438 const DECODE_TABLE: [u8; 256] = {
439 let mut table = [0xFF; 256];
440 let chars = b"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/";
441 let mut i = 0;
442 while i < 64 {
443 table[chars[i] as usize] = i as u8;
444 i += 1;
445 }
446 table
447 };
448
449 let bytes = data.as_bytes();
450 let mut result = Vec::with_capacity((bytes.len() * 3) / 4);
451 let mut buffer = 0u32;
452 let mut bits = 0;
453
454 for &byte in bytes {
455 if byte == b'=' {
456 break;
457 }
458
459 let value = DECODE_TABLE[byte as usize];
460 if value == 0xFF {
461 continue; }
463
464 buffer = (buffer << 6) | (value as u32);
465 bits += 6;
466
467 if bits >= 8 {
468 bits -= 8;
469 result.push((buffer >> bits) as u8);
470 buffer &= (1 << bits) - 1;
471 }
472 }
473
474 Ok(result)
475}
476
477#[derive(Debug, Clone, Serialize, Deserialize)]
483pub struct OpenTelemetryConfig {
484 pub service_name: String,
486
487 pub service_version: String,
489
490 pub otlp_endpoint: Option<String>,
492
493 pub sampling_rate: f64,
495
496 pub enable_export: bool,
498
499 pub export_timeout: u64,
501
502 pub batch_config: BatchConfig,
504}
505
506impl Default for OpenTelemetryConfig {
507 fn default() -> Self {
508 Self {
509 service_name: "oxirs-gql".to_string(),
510 service_version: env!("CARGO_PKG_VERSION").to_string(),
511 otlp_endpoint: None,
512 sampling_rate: 1.0,
513 enable_export: true,
514 export_timeout: 30,
515 batch_config: BatchConfig::default(),
516 }
517 }
518}
519
520#[derive(Debug, Clone, Serialize, Deserialize)]
522pub struct BatchConfig {
523 pub max_queue_size: usize,
525
526 pub max_export_batch_size: usize,
528
529 pub scheduled_delay_millis: u64,
531
532 pub max_export_timeout_millis: u64,
534}
535
536impl Default for BatchConfig {
537 fn default() -> Self {
538 Self {
539 max_queue_size: 2048,
540 max_export_batch_size: 512,
541 scheduled_delay_millis: 5000,
542 max_export_timeout_millis: 30000,
543 }
544 }
545}
546
547#[derive(Debug, Clone, Serialize, Deserialize)]
549pub struct TraceContext {
550 pub trace_id: String,
552
553 pub span_id: String,
555
556 pub parent_span_id: Option<String>,
558
559 pub trace_flags: u8,
561
562 pub baggage: HashMap<String, String>,
564}
565
566impl Default for TraceContext {
567 fn default() -> Self {
568 Self::new()
569 }
570}
571
572impl TraceContext {
573 pub fn new() -> Self {
575 Self {
576 trace_id: generate_trace_id(),
577 span_id: generate_span_id(),
578 parent_span_id: None,
579 trace_flags: 1, baggage: HashMap::new(),
581 }
582 }
583
584 pub fn create_child(&self) -> Self {
586 Self {
587 trace_id: self.trace_id.clone(),
588 span_id: generate_span_id(),
589 parent_span_id: Some(self.span_id.clone()),
590 trace_flags: self.trace_flags,
591 baggage: self.baggage.clone(),
592 }
593 }
594
595 pub fn from_traceparent(header: &str) -> Result<Self> {
597 let parts: Vec<&str> = header.split('-').collect();
598 if parts.len() != 4 {
599 return Err(anyhow!("Invalid traceparent format"));
600 }
601
602 Ok(Self {
603 trace_id: parts[1].to_string(),
604 span_id: parts[2].to_string(),
605 parent_span_id: None,
606 trace_flags: u8::from_str_radix(parts[3], 16)?,
607 baggage: HashMap::new(),
608 })
609 }
610
611 pub fn to_traceparent(&self) -> String {
613 format!(
614 "00-{}-{}-{:02x}",
615 self.trace_id, self.span_id, self.trace_flags
616 )
617 }
618}
619
620fn generate_trace_id() -> String {
621 uuid::Uuid::new_v4().to_string().replace("-", "")
623}
624
625fn generate_span_id() -> String {
626 format!("{:016x}", fastrand::u64(..))
628}
629
630#[derive(Debug, Clone, Serialize, Deserialize)]
636pub struct ConnectionPoolConfig {
637 pub min_connections: usize,
639
640 pub max_connections: usize,
642
643 pub connection_timeout: u64,
645
646 pub idle_timeout: u64,
648
649 pub max_lifetime: u64,
651
652 pub enable_health_check: bool,
654
655 pub health_check_interval: u64,
657}
658
659impl Default for ConnectionPoolConfig {
660 fn default() -> Self {
661 Self {
662 min_connections: 5,
663 max_connections: 100,
664 connection_timeout: 30,
665 idle_timeout: 600, max_lifetime: 1800, enable_health_check: true,
668 health_check_interval: 30,
669 }
670 }
671}
672
673#[derive(Debug, Clone, Serialize, Deserialize)]
675pub struct PoolStats {
676 pub total_connections: usize,
678
679 pub active_connections: usize,
681
682 pub idle_connections: usize,
684
685 pub waiting_requests: usize,
687
688 pub total_requests: u64,
690
691 pub failed_connections: u64,
693}
694
695pub struct ConnectionPool {
697 config: ConnectionPoolConfig,
698 stats: Arc<RwLock<PoolStats>>,
699 last_adjusted: Arc<RwLock<Instant>>,
700}
701
702impl ConnectionPool {
703 pub fn new(config: ConnectionPoolConfig) -> Self {
705 Self {
706 config,
707 stats: Arc::new(RwLock::new(PoolStats {
708 total_connections: 0,
709 active_connections: 0,
710 idle_connections: 0,
711 waiting_requests: 0,
712 total_requests: 0,
713 failed_connections: 0,
714 })),
715 last_adjusted: Arc::new(RwLock::new(Instant::now())),
716 }
717 }
718
719 pub async fn get_stats(&self) -> PoolStats {
721 self.stats.read().await.clone()
722 }
723
724 pub async fn adjust_pool_size(&self, load_factor: f64) -> Result<()> {
726 let mut last_adjusted = self.last_adjusted.write().await;
727 let now = Instant::now();
728
729 if now.duration_since(*last_adjusted) < Duration::from_secs(60) {
731 return Ok(());
732 }
733
734 let mut stats = self.stats.write().await;
735 let current_size = stats.total_connections;
736
737 let target_size = if load_factor > 0.8 {
739 std::cmp::min(
741 current_size + (current_size as f64 * 0.2) as usize,
742 self.config.max_connections,
743 )
744 } else if load_factor < 0.3 && current_size > self.config.min_connections {
745 std::cmp::max(
747 current_size - (current_size as f64 * 0.1) as usize,
748 self.config.min_connections,
749 )
750 } else {
751 current_size
752 };
753
754 if target_size != current_size {
755 info!(
756 "Adjusting connection pool size: {} -> {} (load: {:.2})",
757 current_size, target_size, load_factor
758 );
759 stats.total_connections = target_size;
760 }
761
762 *last_adjusted = now;
763 Ok(())
764 }
765}
766
767#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
773pub enum HealthStatus {
774 Healthy,
776 Degraded,
778 Unhealthy,
780}
781
782impl fmt::Display for HealthStatus {
783 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
784 match self {
785 HealthStatus::Healthy => write!(f, "healthy"),
786 HealthStatus::Degraded => write!(f, "degraded"),
787 HealthStatus::Unhealthy => write!(f, "unhealthy"),
788 }
789 }
790}
791
792#[derive(Debug, Clone, Serialize, Deserialize)]
794pub struct HealthCheckResult {
795 pub status: HealthStatus,
797
798 pub version: String,
800
801 pub uptime: u64,
803
804 pub timestamp: String,
806
807 pub checks: HashMap<String, ComponentHealth>,
809}
810
811#[derive(Debug, Clone, Serialize, Deserialize)]
813pub struct ComponentHealth {
814 pub status: HealthStatus,
816
817 pub response_time_ms: Option<u64>,
819
820 pub error: Option<String>,
822
823 pub metadata: HashMap<String, serde_json::Value>,
825}
826
827pub struct HealthChecker {
829 start_time: Instant,
830 checks: HashMap<String, Box<dyn Fn() -> ComponentHealth + Send + Sync>>,
831}
832
833impl Default for HealthChecker {
834 fn default() -> Self {
835 Self::new()
836 }
837}
838
839impl HealthChecker {
840 pub fn new() -> Self {
842 Self {
843 start_time: Instant::now(),
844 checks: HashMap::new(),
845 }
846 }
847
848 pub fn register_check<F>(&mut self, name: String, check: F)
850 where
851 F: Fn() -> ComponentHealth + Send + Sync + 'static,
852 {
853 self.checks.insert(name, Box::new(check));
854 }
855
856 pub fn check_health(&self) -> HealthCheckResult {
858 let mut checks = HashMap::new();
859 let mut overall_status = HealthStatus::Healthy;
860
861 for (name, check) in &self.checks {
862 let component_health = check();
863
864 match component_health.status {
865 HealthStatus::Unhealthy => overall_status = HealthStatus::Unhealthy,
866 HealthStatus::Degraded if overall_status == HealthStatus::Healthy => {
867 overall_status = HealthStatus::Degraded
868 }
869 _ => {}
870 }
871
872 checks.insert(name.clone(), component_health);
873 }
874
875 HealthCheckResult {
876 status: overall_status,
877 version: env!("CARGO_PKG_VERSION").to_string(),
878 uptime: self.start_time.elapsed().as_secs(),
879 timestamp: chrono::Utc::now().to_rfc3339(),
880 checks,
881 }
882 }
883}
884
885#[derive(Debug, Clone, Serialize, Deserialize)]
891pub struct RequestLog {
892 pub request_id: String,
894
895 pub method: String,
897
898 pub path: String,
900
901 pub query_string: Option<String>,
903
904 pub status_code: u16,
906
907 pub response_time_ms: u64,
909
910 pub request_size: u64,
912
913 pub response_size: u64,
915
916 pub client_ip: String,
918
919 pub user_agent: Option<String>,
921
922 pub timestamp: String,
924
925 pub trace_context: Option<TraceContext>,
927}
928
929impl RequestLog {
930 pub fn new(request_id: String, method: String, path: String, client_ip: String) -> Self {
932 Self {
933 request_id,
934 method,
935 path,
936 query_string: None,
937 status_code: 200,
938 response_time_ms: 0,
939 request_size: 0,
940 response_size: 0,
941 client_ip,
942 user_agent: None,
943 timestamp: chrono::Utc::now().to_rfc3339(),
944 trace_context: None,
945 }
946 }
947
948 pub fn to_json(&self) -> Result<String> {
950 Ok(serde_json::to_string(self)?)
951 }
952}
953
954#[derive(Debug, Clone)]
960pub struct Metrics {
961 pub requests_total: u64,
963
964 pub errors_total: u64,
966
967 pub request_duration_ms: Vec<u64>,
969
970 pub active_connections: usize,
972
973 pub pool_size: usize,
975}
976
977impl Default for Metrics {
978 fn default() -> Self {
979 Self::new()
980 }
981}
982
983impl Metrics {
984 pub fn new() -> Self {
986 Self {
987 requests_total: 0,
988 errors_total: 0,
989 request_duration_ms: Vec::new(),
990 active_connections: 0,
991 pool_size: 0,
992 }
993 }
994
995 pub fn to_prometheus(&self) -> String {
997 let mut output = String::new();
998
999 output.push_str("# HELP oxirs_gql_requests_total Total number of requests\n");
1001 output.push_str("# TYPE oxirs_gql_requests_total counter\n");
1002 output.push_str(&format!(
1003 "oxirs_gql_requests_total {}\n",
1004 self.requests_total
1005 ));
1006
1007 output.push_str("# HELP oxirs_gql_errors_total Total number of errors\n");
1009 output.push_str("# TYPE oxirs_gql_errors_total counter\n");
1010 output.push_str(&format!("oxirs_gql_errors_total {}\n", self.errors_total));
1011
1012 output.push_str("# HELP oxirs_gql_active_connections Current active connections\n");
1014 output.push_str("# TYPE oxirs_gql_active_connections gauge\n");
1015 output.push_str(&format!(
1016 "oxirs_gql_active_connections {}\n",
1017 self.active_connections
1018 ));
1019
1020 output.push_str("# HELP oxirs_gql_pool_size Current connection pool size\n");
1022 output.push_str("# TYPE oxirs_gql_pool_size gauge\n");
1023 output.push_str(&format!("oxirs_gql_pool_size {}\n", self.pool_size));
1024
1025 output
1026 }
1027}
1028
1029#[derive(Debug, Clone, Copy, PartialEq, Eq)]
1035pub enum CircuitState {
1036 Closed,
1038 Open,
1040 HalfOpen,
1042}
1043
1044#[derive(Debug, Clone, Serialize, Deserialize)]
1046pub struct CircuitBreakerConfig {
1047 pub failure_threshold: usize,
1049
1050 pub success_threshold: usize,
1052
1053 pub timeout: u64,
1055
1056 pub window_size: u64,
1058}
1059
1060impl Default for CircuitBreakerConfig {
1061 fn default() -> Self {
1062 Self {
1063 failure_threshold: 5,
1064 success_threshold: 2,
1065 timeout: 60,
1066 window_size: 60,
1067 }
1068 }
1069}
1070
1071pub struct CircuitBreaker {
1073 config: CircuitBreakerConfig,
1074 state: Arc<RwLock<CircuitState>>,
1075 failures: Arc<RwLock<usize>>,
1076 successes: Arc<RwLock<usize>>,
1077 last_failure_time: Arc<RwLock<Option<Instant>>>,
1078}
1079
1080impl CircuitBreaker {
1081 pub fn new(config: CircuitBreakerConfig) -> Self {
1083 Self {
1084 config,
1085 state: Arc::new(RwLock::new(CircuitState::Closed)),
1086 failures: Arc::new(RwLock::new(0)),
1087 successes: Arc::new(RwLock::new(0)),
1088 last_failure_time: Arc::new(RwLock::new(None)),
1089 }
1090 }
1091
1092 pub async fn is_allowed(&self) -> bool {
1094 let state = *self.state.read().await;
1095
1096 match state {
1097 CircuitState::Closed => true,
1098 CircuitState::Open => {
1099 if let Some(last_failure) = *self.last_failure_time.read().await {
1101 if last_failure.elapsed() > Duration::from_secs(self.config.timeout) {
1102 *self.state.write().await = CircuitState::HalfOpen;
1104 true
1105 } else {
1106 false
1107 }
1108 } else {
1109 false
1110 }
1111 }
1112 CircuitState::HalfOpen => true,
1113 }
1114 }
1115
1116 pub async fn record_success(&self) {
1118 let state = *self.state.read().await;
1119
1120 match state {
1121 CircuitState::HalfOpen => {
1122 let mut successes = self.successes.write().await;
1123 *successes += 1;
1124
1125 if *successes >= self.config.success_threshold {
1126 *self.state.write().await = CircuitState::Closed;
1127 *successes = 0;
1128 *self.failures.write().await = 0;
1129 info!("Circuit breaker closed after successful recovery");
1130 }
1131 }
1132 CircuitState::Closed => {
1133 *self.failures.write().await = 0;
1135 }
1136 _ => {}
1137 }
1138 }
1139
1140 pub async fn record_failure(&self) {
1142 let state = *self.state.read().await;
1143
1144 match state {
1145 CircuitState::Closed | CircuitState::HalfOpen => {
1146 let mut failures = self.failures.write().await;
1147 *failures += 1;
1148
1149 if *failures >= self.config.failure_threshold {
1150 *self.state.write().await = CircuitState::Open;
1151 *self.last_failure_time.write().await = Some(Instant::now());
1152 *self.successes.write().await = 0;
1153 warn!("Circuit breaker opened due to failures");
1154 }
1155 }
1156 _ => {}
1157 }
1158 }
1159
1160 pub async fn get_state(&self) -> CircuitState {
1162 *self.state.read().await
1163 }
1164}
1165
1166#[cfg(test)]
1167mod tests {
1168 use super::*;
1169
1170 #[test]
1171 fn test_cors_config_default() {
1172 let config = CorsConfig::default();
1173 assert!(config.is_origin_allowed("https://example.com"));
1174 assert_eq!(config.max_age, Some(3600));
1175 }
1176
1177 #[test]
1178 fn test_cors_origin_check() {
1179 let config = CorsConfig::production(vec!["https://example.com".to_string()]);
1180 assert!(config.is_origin_allowed("https://example.com"));
1181 assert!(!config.is_origin_allowed("https://evil.com"));
1182 }
1183
1184 #[test]
1185 fn test_cors_wildcard_matching() {
1186 let config = CorsConfig {
1187 allowed_origins: vec!["https://*.example.com".to_string()],
1188 wildcard_origins: true,
1189 ..Default::default()
1190 };
1191
1192 assert!(config.is_origin_allowed("https://app.example.com"));
1193 assert!(config.is_origin_allowed("https://api.example.com"));
1194 assert!(!config.is_origin_allowed("https://example.com"));
1195 }
1196
1197 #[test]
1198 fn test_jwt_algorithm_display() {
1199 assert_eq!(JwtAlgorithm::HS256.to_string(), "HS256");
1200 assert_eq!(JwtAlgorithm::RS512.to_string(), "RS512");
1201 }
1202
1203 #[test]
1204 fn test_jwt_claims_creation() {
1205 let claims = JwtClaims::new("user123".to_string(), 3600);
1206 assert_eq!(claims.sub, "user123");
1207 assert!(!claims.is_expired());
1208 }
1209
1210 #[test]
1211 fn test_jwt_claims_custom() {
1212 let claims = JwtClaims::new("user123".to_string(), 3600)
1213 .with_claim("role".to_string(), serde_json::json!("admin"));
1214
1215 assert_eq!(
1216 claims.custom.get("role").expect("should succeed"),
1217 &serde_json::json!("admin")
1218 );
1219 }
1220
1221 #[test]
1222 fn test_trace_context_creation() {
1223 let ctx = TraceContext::new();
1224 assert!(!ctx.trace_id.is_empty());
1225 assert!(!ctx.span_id.is_empty());
1226 assert_eq!(ctx.trace_flags, 1);
1227 }
1228
1229 #[test]
1230 fn test_trace_context_child() {
1231 let parent = TraceContext::new();
1232 let child = parent.create_child();
1233
1234 assert_eq!(child.trace_id, parent.trace_id);
1235 assert_ne!(child.span_id, parent.span_id);
1236 assert_eq!(child.parent_span_id, Some(parent.span_id.clone()));
1237 }
1238
1239 #[test]
1240 fn test_trace_context_traceparent() {
1241 let ctx = TraceContext::new();
1242 let header = ctx.to_traceparent();
1243
1244 assert!(header.starts_with("00-"));
1245 assert_eq!(header.split('-').count(), 4);
1246 }
1247
1248 #[test]
1249 fn test_connection_pool_config() {
1250 let config = ConnectionPoolConfig::default();
1251 assert_eq!(config.min_connections, 5);
1252 assert_eq!(config.max_connections, 100);
1253 }
1254
1255 #[tokio::test]
1256 async fn test_connection_pool_stats() {
1257 let pool = ConnectionPool::new(ConnectionPoolConfig::default());
1258 let stats = pool.get_stats().await;
1259
1260 assert_eq!(stats.total_connections, 0);
1261 assert_eq!(stats.active_connections, 0);
1262 }
1263
1264 #[test]
1265 fn test_health_status_display() {
1266 assert_eq!(HealthStatus::Healthy.to_string(), "healthy");
1267 assert_eq!(HealthStatus::Degraded.to_string(), "degraded");
1268 assert_eq!(HealthStatus::Unhealthy.to_string(), "unhealthy");
1269 }
1270
1271 #[test]
1272 fn test_health_checker() {
1273 let mut checker = HealthChecker::new();
1274
1275 checker.register_check("database".to_string(), || ComponentHealth {
1276 status: HealthStatus::Healthy,
1277 response_time_ms: Some(5),
1278 error: None,
1279 metadata: HashMap::new(),
1280 });
1281
1282 let result = checker.check_health();
1283 assert_eq!(result.status, HealthStatus::Healthy);
1284 assert!(result.checks.contains_key("database"));
1285 }
1286
1287 #[test]
1288 fn test_request_log_creation() {
1289 let log = RequestLog::new(
1290 "req-123".to_string(),
1291 "POST".to_string(),
1292 "/graphql".to_string(),
1293 "127.0.0.1".to_string(),
1294 );
1295
1296 assert_eq!(log.request_id, "req-123");
1297 assert_eq!(log.method, "POST");
1298 assert_eq!(log.status_code, 200);
1299 }
1300
1301 #[test]
1302 fn test_metrics_prometheus_export() {
1303 let mut metrics = Metrics::new();
1304 metrics.requests_total = 100;
1305 metrics.errors_total = 5;
1306 metrics.active_connections = 10;
1307
1308 let output = metrics.to_prometheus();
1309 assert!(output.contains("oxirs_gql_requests_total 100"));
1310 assert!(output.contains("oxirs_gql_errors_total 5"));
1311 assert!(output.contains("oxirs_gql_active_connections 10"));
1312 }
1313
1314 #[test]
1315 fn test_circuit_breaker_config() {
1316 let config = CircuitBreakerConfig::default();
1317 assert_eq!(config.failure_threshold, 5);
1318 assert_eq!(config.success_threshold, 2);
1319 }
1320
1321 #[tokio::test]
1322 async fn test_circuit_breaker_state() {
1323 let breaker = CircuitBreaker::new(CircuitBreakerConfig::default());
1324
1325 assert!(breaker.is_allowed().await);
1326 assert_eq!(breaker.get_state().await, CircuitState::Closed);
1327 }
1328
1329 #[tokio::test]
1330 async fn test_circuit_breaker_failure() {
1331 let config = CircuitBreakerConfig {
1332 failure_threshold: 2,
1333 ..Default::default()
1334 };
1335
1336 let breaker = CircuitBreaker::new(config);
1337
1338 breaker.record_failure().await;
1339 assert_eq!(breaker.get_state().await, CircuitState::Closed);
1340
1341 breaker.record_failure().await;
1342 assert_eq!(breaker.get_state().await, CircuitState::Open);
1343 assert!(!breaker.is_allowed().await);
1344 }
1345
1346 #[tokio::test]
1347 async fn test_circuit_breaker_recovery() {
1348 let config = CircuitBreakerConfig {
1349 failure_threshold: 1,
1350 success_threshold: 1,
1351 timeout: 0, ..Default::default()
1353 };
1354
1355 let breaker = CircuitBreaker::new(config);
1356
1357 breaker.record_failure().await;
1358 assert_eq!(breaker.get_state().await, CircuitState::Open);
1359
1360 tokio::time::sleep(Duration::from_millis(100)).await;
1362
1363 assert!(breaker.is_allowed().await);
1364 assert_eq!(breaker.get_state().await, CircuitState::HalfOpen);
1365
1366 breaker.record_success().await;
1367 assert_eq!(breaker.get_state().await, CircuitState::Closed);
1368 }
1369}