Skip to main content

oxirs_gql/
production.rs

1//! Production-Ready Features for Enterprise Deployment
2//!
3//! This module provides comprehensive production features:
4//! - **CORS Configuration**: Fine-grained CORS control with preflight caching
5//! - **JWT Authentication**: RSA/HMAC-based JWT with automatic refresh
6//! - **OpenTelemetry Tracing**: Distributed tracing with automatic span propagation
7//! - **Connection Pooling**: Adaptive connection pool with health monitoring
8//! - **Health Checks**: Comprehensive health endpoints with dependencies
9//! - **Request Logging**: Structured logging with correlation IDs
10//! - **Metrics Export**: Prometheus-compatible metrics endpoint
11//! - **Rate Limiting Integration**: Production-grade rate limiting
12//! - **Circuit Breaker**: Automatic failure detection and recovery
13
14use 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
23// SciRS2 Core integration for secure random generation and metrics
24use scirs2_core::random::secure::SecureRandom;
25
26// Cryptography for JWT
27use sha2::Sha256;
28
29// ============================================================================
30// CORS Configuration
31// ============================================================================
32
33/// Comprehensive CORS configuration
34#[derive(Debug, Clone, Serialize, Deserialize)]
35pub struct CorsConfig {
36    /// Allowed origins (e.g., ["<https://example.com>", "<http://localhost:3000>"])
37    /// Use "*" for all origins (not recommended for production)
38    pub allowed_origins: Vec<String>,
39
40    /// Allowed HTTP methods
41    pub allowed_methods: Vec<String>,
42
43    /// Allowed headers
44    pub allowed_headers: Vec<String>,
45
46    /// Exposed headers (headers that browser can access)
47    pub exposed_headers: Vec<String>,
48
49    /// Allow credentials (cookies, authorization headers)
50    pub allow_credentials: bool,
51
52    /// Max age for preflight cache (in seconds)
53    pub max_age: Option<u64>,
54
55    /// Whether to use wildcard origin matching
56    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), // 1 hour
72            wildcard_origins: false,
73        }
74    }
75}
76
77impl CorsConfig {
78    /// Create production-ready CORS configuration
79    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), // 24 hours
92            wildcard_origins: false,
93        }
94    }
95
96    /// Check if origin is allowed
97    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    /// Wildcard pattern matching for origins
114    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// ============================================================================
129// JWT Authentication
130// ============================================================================
131
132/// JWT authentication configuration
133#[derive(Debug, Clone, Serialize, Deserialize)]
134pub struct JwtConfig {
135    /// JWT secret or public key for verification
136    pub secret: String,
137
138    /// JWT signing algorithm (HS256, HS384, HS512, RS256, RS384, RS512)
139    pub algorithm: JwtAlgorithm,
140
141    /// Token expiration time (in seconds)
142    pub expiration: u64,
143
144    /// Token issuer
145    pub issuer: Option<String>,
146
147    /// Token audience
148    pub audience: Option<Vec<String>>,
149
150    /// Required claims
151    pub required_claims: Vec<String>,
152
153    /// Enable automatic token refresh
154    pub enable_refresh: bool,
155
156    /// Refresh token expiration (in seconds)
157    pub refresh_expiration: u64,
158}
159
160impl Default for JwtConfig {
161    fn default() -> Self {
162        Self {
163            secret: "".to_string(), // Must be set by user
164            algorithm: JwtAlgorithm::HS256,
165            expiration: 3600, // 1 hour
166            issuer: None,
167            audience: None,
168            required_claims: vec!["sub".to_string()],
169            enable_refresh: true,
170            refresh_expiration: 604800, // 7 days
171        }
172    }
173}
174
175/// JWT signing algorithms
176#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
177pub enum JwtAlgorithm {
178    /// HMAC with SHA-256
179    HS256,
180    /// HMAC with SHA-384
181    HS384,
182    /// HMAC with SHA-512
183    HS512,
184    /// RSA with SHA-256
185    RS256,
186    /// RSA with SHA-384
187    RS384,
188    /// RSA with SHA-512
189    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/// JWT token claims
206#[derive(Debug, Clone, Serialize, Deserialize)]
207pub struct JwtClaims {
208    /// Subject (user ID)
209    pub sub: String,
210
211    /// Expiration time (as UTC timestamp)
212    pub exp: u64,
213
214    /// Issued at time (as UTC timestamp)
215    pub iat: u64,
216
217    /// Issuer
218    #[serde(skip_serializing_if = "Option::is_none")]
219    pub iss: Option<String>,
220
221    /// Audience
222    #[serde(skip_serializing_if = "Option::is_none")]
223    pub aud: Option<Vec<String>>,
224
225    /// Custom claims
226    #[serde(flatten)]
227    pub custom: HashMap<String, serde_json::Value>,
228}
229
230impl JwtClaims {
231    /// Create new JWT claims
232    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    /// Add custom claim
249    pub fn with_claim(mut self, key: String, value: serde_json::Value) -> Self {
250        self.custom.insert(key, value);
251        self
252    }
253
254    /// Check if token is expired
255    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
264/// JWT token manager
265pub struct JwtManager {
266    config: JwtConfig,
267}
268
269impl JwtManager {
270    /// Create new JWT manager
271    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    /// Generate a cryptographically secure JWT secret using scirs2_core
279    ///
280    /// This generates a 64-character alphanumeric secret suitable for HMAC-SHA256 signing.
281    /// The secret has high entropy and is suitable for production use.
282    ///
283    /// # Example
284    /// ```rust,ignore
285    /// let secret = JwtManager::generate_secure_secret();
286    /// let config = JwtConfig {
287    ///     secret,
288    ///     algorithm: JwtAlgorithm::HS256,
289    ///     expiration: 3600,
290    /// };
291    /// ```
292    pub fn generate_secure_secret() -> String {
293        let mut rng = SecureRandom::new();
294        rng.random_alphanumeric(64)
295    }
296
297    /// Generate JWT token with HMAC-SHA256 signing
298    ///
299    /// This implementation uses HMAC-SHA256 for signing JWT tokens.
300    /// The token format is: `header.payload.signature` (standard JWT format)
301    ///
302    /// For production use with additional algorithms (RS256, ES256), consider using the `jsonwebtoken` crate.
303    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        // Create JWT header
309        let header = serde_json::json!({
310            "typ": "JWT",
311            "alg": self.config.algorithm.to_string()
312        });
313
314        // Encode header and payload
315        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        // Create signing input
319        let signing_input = format!("{}.{}", header_b64, payload_b64);
320
321        // Generate signature using HMAC-SHA256
322        let signature = self.sign_hmac_sha256(&signing_input)?;
323        let signature_b64 = base64_url_encode(&hex::encode(&signature));
324
325        // Construct final JWT token
326        Ok(format!("{}.{}", signing_input, signature_b64))
327    }
328
329    /// Sign data using HMAC-SHA256
330    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    /// Verify JWT token with HMAC-SHA256 signature validation
342    ///
343    /// This implementation verifies JWT tokens signed with HMAC-SHA256.
344    /// It checks:
345    /// 1. Token format (header.payload.signature)
346    /// 2. Signature validity
347    /// 3. Token expiration
348    ///
349    /// For production use with additional algorithms (RS256, ES256), consider using the `jsonwebtoken` crate.
350    pub fn verify_token(&self, token: &str) -> Result<JwtClaims> {
351        // Split token into parts
352        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        // Verify signature
361        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        // Decode and parse payload
370        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        // Check expiration
375        if claims.is_expired() {
376            return Err(anyhow!("Token expired at {}", claims.exp));
377        }
378
379        Ok(claims)
380    }
381}
382
383// Base64 URL-safe encoding/decoding for JWT
384// Note: JWT uses URL-safe base64 (RFC 4648 §5) without padding
385
386/// Base64 URL-safe encoding (no padding)
387fn base64_url_encode(data: &str) -> String {
388    // Simple base64 URL-safe encoding
389    // In production, consider using the `base64` crate for better performance
390    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    // JWT doesn't use padding
414    result
415}
416
417/// Base64 URL-safe decoding (no padding)
418fn base64_url_decode(data: &str) -> Result<String> {
419    // Simple base64 URL-safe decoding
420    // In production, consider using the `base64` crate for better performance
421    let data = data.replace('-', "+").replace('_', "/");
422
423    // Add padding if needed
424    let padding = match data.len() % 4 {
425        2 => "==",
426        3 => "=",
427        _ => "",
428    };
429    let data_with_padding = format!("{}{}", data, padding);
430
431    // Decode base64
432    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
436/// Internal base64 decoding helper
437fn 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; // Skip invalid characters
462        }
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// ============================================================================
478// OpenTelemetry Tracing
479// ============================================================================
480
481/// OpenTelemetry configuration
482#[derive(Debug, Clone, Serialize, Deserialize)]
483pub struct OpenTelemetryConfig {
484    /// Service name
485    pub service_name: String,
486
487    /// Service version
488    pub service_version: String,
489
490    /// OTLP endpoint (e.g., "http://localhost:4317")
491    pub otlp_endpoint: Option<String>,
492
493    /// Sampling rate (0.0 to 1.0)
494    pub sampling_rate: f64,
495
496    /// Enable trace export
497    pub enable_export: bool,
498
499    /// Export timeout (in seconds)
500    pub export_timeout: u64,
501
502    /// Batch span processor configuration
503    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/// Batch span processor configuration
521#[derive(Debug, Clone, Serialize, Deserialize)]
522pub struct BatchConfig {
523    /// Maximum queue size
524    pub max_queue_size: usize,
525
526    /// Maximum batch size
527    pub max_export_batch_size: usize,
528
529    /// Scheduled delay (in milliseconds)
530    pub scheduled_delay_millis: u64,
531
532    /// Maximum export timeout (in milliseconds)
533    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/// Trace context for distributed tracing
548#[derive(Debug, Clone, Serialize, Deserialize)]
549pub struct TraceContext {
550    /// Trace ID (128-bit)
551    pub trace_id: String,
552
553    /// Span ID (64-bit)
554    pub span_id: String,
555
556    /// Parent span ID
557    pub parent_span_id: Option<String>,
558
559    /// Trace flags
560    pub trace_flags: u8,
561
562    /// Custom baggage
563    pub baggage: HashMap<String, String>,
564}
565
566impl Default for TraceContext {
567    fn default() -> Self {
568        Self::new()
569    }
570}
571
572impl TraceContext {
573    /// Create new trace context
574    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, // Sampled
580            baggage: HashMap::new(),
581        }
582    }
583
584    /// Create child context
585    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    /// Parse from W3C traceparent header
596    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    /// Convert to W3C traceparent header
612    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    // Use UUID v4 for simplicity (128-bit)
622    uuid::Uuid::new_v4().to_string().replace("-", "")
623}
624
625fn generate_span_id() -> String {
626    // Generate 64-bit ID
627    format!("{:016x}", fastrand::u64(..))
628}
629
630// ============================================================================
631// Connection Pooling
632// ============================================================================
633
634/// Connection pool configuration
635#[derive(Debug, Clone, Serialize, Deserialize)]
636pub struct ConnectionPoolConfig {
637    /// Minimum number of connections
638    pub min_connections: usize,
639
640    /// Maximum number of connections
641    pub max_connections: usize,
642
643    /// Connection timeout (in seconds)
644    pub connection_timeout: u64,
645
646    /// Idle timeout (in seconds)
647    pub idle_timeout: u64,
648
649    /// Maximum lifetime of connection (in seconds)
650    pub max_lifetime: u64,
651
652    /// Enable connection health checks
653    pub enable_health_check: bool,
654
655    /// Health check interval (in seconds)
656    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,  // 10 minutes
666            max_lifetime: 1800, // 30 minutes
667            enable_health_check: true,
668            health_check_interval: 30,
669        }
670    }
671}
672
673/// Connection pool statistics
674#[derive(Debug, Clone, Serialize, Deserialize)]
675pub struct PoolStats {
676    /// Total connections
677    pub total_connections: usize,
678
679    /// Active connections
680    pub active_connections: usize,
681
682    /// Idle connections
683    pub idle_connections: usize,
684
685    /// Waiting requests
686    pub waiting_requests: usize,
687
688    /// Total requests served
689    pub total_requests: u64,
690
691    /// Failed connection attempts
692    pub failed_connections: u64,
693}
694
695/// Adaptive connection pool
696pub struct ConnectionPool {
697    config: ConnectionPoolConfig,
698    stats: Arc<RwLock<PoolStats>>,
699    last_adjusted: Arc<RwLock<Instant>>,
700}
701
702impl ConnectionPool {
703    /// Create new connection pool
704    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    /// Get pool statistics
720    pub async fn get_stats(&self) -> PoolStats {
721        self.stats.read().await.clone()
722    }
723
724    /// Adjust pool size based on load
725    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        // Only adjust every 60 seconds
730        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        // Calculate target size based on load
738        let target_size = if load_factor > 0.8 {
739            // High load - increase pool size
740            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            // Low load - decrease pool size
746            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// ============================================================================
768// Health Checks
769// ============================================================================
770
771/// Health check status
772#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
773pub enum HealthStatus {
774    /// Service is healthy
775    Healthy,
776    /// Service is degraded but operational
777    Degraded,
778    /// Service is unhealthy
779    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/// Health check result
793#[derive(Debug, Clone, Serialize, Deserialize)]
794pub struct HealthCheckResult {
795    /// Overall status
796    pub status: HealthStatus,
797
798    /// Service version
799    pub version: String,
800
801    /// Uptime (in seconds)
802    pub uptime: u64,
803
804    /// Timestamp
805    pub timestamp: String,
806
807    /// Individual component checks
808    pub checks: HashMap<String, ComponentHealth>,
809}
810
811/// Component health status
812#[derive(Debug, Clone, Serialize, Deserialize)]
813pub struct ComponentHealth {
814    /// Component status
815    pub status: HealthStatus,
816
817    /// Response time (in milliseconds)
818    pub response_time_ms: Option<u64>,
819
820    /// Error message (if unhealthy)
821    pub error: Option<String>,
822
823    /// Additional metadata
824    pub metadata: HashMap<String, serde_json::Value>,
825}
826
827/// Health checker
828pub 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    /// Create new health checker
841    pub fn new() -> Self {
842        Self {
843            start_time: Instant::now(),
844            checks: HashMap::new(),
845        }
846    }
847
848    /// Register component health check
849    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    /// Perform health check
857    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// ============================================================================
886// Request Logging
887// ============================================================================
888
889/// Request log entry
890#[derive(Debug, Clone, Serialize, Deserialize)]
891pub struct RequestLog {
892    /// Request ID
893    pub request_id: String,
894
895    /// HTTP method
896    pub method: String,
897
898    /// Request path
899    pub path: String,
900
901    /// Query string
902    pub query_string: Option<String>,
903
904    /// Status code
905    pub status_code: u16,
906
907    /// Response time (in milliseconds)
908    pub response_time_ms: u64,
909
910    /// Request size (in bytes)
911    pub request_size: u64,
912
913    /// Response size (in bytes)
914    pub response_size: u64,
915
916    /// Client IP
917    pub client_ip: String,
918
919    /// User agent
920    pub user_agent: Option<String>,
921
922    /// Timestamp
923    pub timestamp: String,
924
925    /// Trace context
926    pub trace_context: Option<TraceContext>,
927}
928
929impl RequestLog {
930    /// Create new request log
931    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    /// Log as JSON
949    pub fn to_json(&self) -> Result<String> {
950        Ok(serde_json::to_string(self)?)
951    }
952}
953
954// ============================================================================
955// Metrics Export
956// ============================================================================
957
958/// Prometheus-compatible metrics
959#[derive(Debug, Clone)]
960pub struct Metrics {
961    /// Request counter
962    pub requests_total: u64,
963
964    /// Error counter
965    pub errors_total: u64,
966
967    /// Request duration histogram (in milliseconds)
968    pub request_duration_ms: Vec<u64>,
969
970    /// Active connections gauge
971    pub active_connections: usize,
972
973    /// Pool size gauge
974    pub pool_size: usize,
975}
976
977impl Default for Metrics {
978    fn default() -> Self {
979        Self::new()
980    }
981}
982
983impl Metrics {
984    /// Create new metrics
985    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    /// Export as Prometheus format
996    pub fn to_prometheus(&self) -> String {
997        let mut output = String::new();
998
999        // Request counter
1000        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        // Error counter
1008        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        // Active connections
1013        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        // Pool size
1021        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// ============================================================================
1030// Circuit Breaker
1031// ============================================================================
1032
1033/// Circuit breaker state
1034#[derive(Debug, Clone, Copy, PartialEq, Eq)]
1035pub enum CircuitState {
1036    /// Circuit is closed (normal operation)
1037    Closed,
1038    /// Circuit is open (failing fast)
1039    Open,
1040    /// Circuit is half-open (testing recovery)
1041    HalfOpen,
1042}
1043
1044/// Circuit breaker configuration
1045#[derive(Debug, Clone, Serialize, Deserialize)]
1046pub struct CircuitBreakerConfig {
1047    /// Failure threshold (number of failures before opening)
1048    pub failure_threshold: usize,
1049
1050    /// Success threshold (number of successes before closing)
1051    pub success_threshold: usize,
1052
1053    /// Timeout before entering half-open state (in seconds)
1054    pub timeout: u64,
1055
1056    /// Window size for failure counting (in seconds)
1057    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
1071/// Circuit breaker for automatic failure recovery
1072pub 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    /// Create new circuit breaker
1082    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    /// Check if operation is allowed
1093    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                // Check if timeout has passed
1100                if let Some(last_failure) = *self.last_failure_time.read().await {
1101                    if last_failure.elapsed() > Duration::from_secs(self.config.timeout) {
1102                        // Move to half-open state
1103                        *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    /// Record success
1117    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                // Reset failure count on success
1134                *self.failures.write().await = 0;
1135            }
1136            _ => {}
1137        }
1138    }
1139
1140    /// Record failure
1141    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    /// Get current state
1161    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, // Immediate recovery test
1352            ..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        // Simulate timeout
1361        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}