use serde::{Deserialize, Serialize};
use std::sync::Arc;
use std::time::{Duration, Instant};
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ServiceConfig {
pub name: String,
pub version: String,
pub address: String,
pub port: u16,
pub request_logging: bool,
pub metrics_enabled: bool,
pub default_timeout: Duration,
pub max_body_size: usize,
pub cors_enabled: bool,
pub cors_origins: Vec<String>,
}
impl Default for ServiceConfig {
fn default() -> Self {
Self {
name: "rustkernels".to_string(),
version: env!("CARGO_PKG_VERSION").to_string(),
address: "0.0.0.0".to_string(),
port: 8080,
request_logging: true,
metrics_enabled: true,
default_timeout: Duration::from_secs(30),
max_body_size: 10 * 1024 * 1024, cors_enabled: true,
cors_origins: vec!["*".to_string()],
}
}
}
impl ServiceConfig {
pub fn development() -> Self {
Self {
name: "rustkernels-dev".to_string(),
address: "127.0.0.1".to_string(),
request_logging: true,
..Default::default()
}
}
pub fn production() -> Self {
Self {
request_logging: false, cors_origins: vec![], ..Default::default()
}
}
pub fn bind_address(&self) -> String {
format!("{}:{}", self.address, self.port)
}
}
#[derive(Debug, Clone)]
pub struct RequestContext {
pub request_id: String,
pub start_time: Instant,
pub trace_id: Option<String>,
pub span_id: Option<String>,
pub tenant_id: Option<String>,
pub user_id: Option<String>,
pub path: String,
pub method: String,
}
impl RequestContext {
pub fn new(path: impl Into<String>, method: impl Into<String>) -> Self {
Self {
request_id: uuid::Uuid::new_v4().to_string(),
start_time: Instant::now(),
trace_id: None,
span_id: None,
tenant_id: None,
user_id: None,
path: path.into(),
method: method.into(),
}
}
pub fn with_trace_id(mut self, trace_id: impl Into<String>) -> Self {
self.trace_id = Some(trace_id.into());
self
}
pub fn with_tenant_id(mut self, tenant_id: impl Into<String>) -> Self {
self.tenant_id = Some(tenant_id.into());
self
}
pub fn with_user_id(mut self, user_id: impl Into<String>) -> Self {
self.user_id = Some(user_id.into());
self
}
pub fn elapsed(&self) -> Duration {
self.start_time.elapsed()
}
pub fn elapsed_us(&self) -> u64 {
self.start_time.elapsed().as_micros() as u64
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct RateLimitConfig {
pub enabled: bool,
pub requests_per_second: u32,
pub burst_size: u32,
pub per_tenant: bool,
}
impl Default for RateLimitConfig {
fn default() -> Self {
Self {
enabled: true,
requests_per_second: 100,
burst_size: 200,
per_tenant: true,
}
}
}
pub struct ServiceMetrics {
total_requests: std::sync::atomic::AtomicU64,
total_errors: std::sync::atomic::AtomicU64,
total_latency_us: std::sync::atomic::AtomicU64,
min_latency_us: std::sync::atomic::AtomicU64,
max_latency_us: std::sync::atomic::AtomicU64,
}
impl ServiceMetrics {
pub fn new() -> Arc<Self> {
Arc::new(Self {
total_requests: std::sync::atomic::AtomicU64::new(0),
total_errors: std::sync::atomic::AtomicU64::new(0),
total_latency_us: std::sync::atomic::AtomicU64::new(0),
min_latency_us: std::sync::atomic::AtomicU64::new(u64::MAX),
max_latency_us: std::sync::atomic::AtomicU64::new(0),
})
}
pub fn record_request(&self, latency_us: u64, is_error: bool) {
use std::sync::atomic::Ordering;
self.total_requests.fetch_add(1, Ordering::Relaxed);
self.total_latency_us
.fetch_add(latency_us, Ordering::Relaxed);
if is_error {
self.total_errors.fetch_add(1, Ordering::Relaxed);
}
self.min_latency_us.fetch_min(latency_us, Ordering::Relaxed);
self.max_latency_us.fetch_max(latency_us, Ordering::Relaxed);
}
pub fn request_count(&self) -> u64 {
self.total_requests
.load(std::sync::atomic::Ordering::Relaxed)
}
pub fn error_count(&self) -> u64 {
self.total_errors.load(std::sync::atomic::Ordering::Relaxed)
}
pub fn avg_latency_us(&self) -> f64 {
use std::sync::atomic::Ordering;
let total = self.total_latency_us.load(Ordering::Relaxed) as f64;
let count = self.total_requests.load(Ordering::Relaxed) as f64;
if count > 0.0 { total / count } else { 0.0 }
}
pub fn min_latency_us(&self) -> u64 {
let val = self
.min_latency_us
.load(std::sync::atomic::Ordering::Relaxed);
if val == u64::MAX { 0 } else { val }
}
pub fn max_latency_us(&self) -> u64 {
self.max_latency_us
.load(std::sync::atomic::Ordering::Relaxed)
}
}
impl Default for ServiceMetrics {
fn default() -> Self {
Self {
total_requests: std::sync::atomic::AtomicU64::new(0),
total_errors: std::sync::atomic::AtomicU64::new(0),
total_latency_us: std::sync::atomic::AtomicU64::new(0),
min_latency_us: std::sync::atomic::AtomicU64::new(u64::MAX),
max_latency_us: std::sync::atomic::AtomicU64::new(0),
}
}
}
pub mod paths {
pub const HEALTH: &str = "/health";
pub const LIVENESS: &str = "/health/live";
pub const READINESS: &str = "/health/ready";
pub const METRICS: &str = "/metrics";
pub const KERNEL_EXECUTE: &str = "/api/v1/kernels/:kernel_id/execute";
pub const KERNEL_LIST: &str = "/api/v1/kernels";
pub const KERNEL_INFO: &str = "/api/v1/kernels/:kernel_id";
}
pub mod headers {
pub const X_REQUEST_ID: &str = "X-Request-ID";
pub const TRACEPARENT: &str = "traceparent";
pub const X_TENANT_ID: &str = "X-Tenant-ID";
pub const X_API_KEY: &str = "X-API-Key";
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_service_config() {
let config = ServiceConfig::default();
assert_eq!(config.bind_address(), "0.0.0.0:8080");
}
#[test]
fn test_request_context() {
let ctx = RequestContext::new("/api/v1/kernels", "POST")
.with_tenant_id("tenant-123")
.with_user_id("user-456");
assert!(!ctx.request_id.is_empty());
assert_eq!(ctx.tenant_id, Some("tenant-123".to_string()));
assert_eq!(ctx.user_id, Some("user-456".to_string()));
}
#[test]
fn test_service_metrics() {
let metrics = ServiceMetrics::new();
assert_eq!(metrics.min_latency_us(), 0);
assert_eq!(metrics.max_latency_us(), 0);
metrics.record_request(1000, false);
metrics.record_request(2000, false);
metrics.record_request(3000, true);
assert_eq!(metrics.request_count(), 3);
assert_eq!(metrics.error_count(), 1);
assert!((metrics.avg_latency_us() - 2000.0).abs() < 0.1);
assert_eq!(metrics.min_latency_us(), 1000);
assert_eq!(metrics.max_latency_us(), 3000);
}
}