1use serde::{Deserialize, Serialize};
4use std::sync::Arc;
5use std::time::{Duration, Instant};
6
7#[derive(Debug, Clone, Serialize, Deserialize)]
9pub struct ServiceConfig {
10 pub name: String,
12 pub version: String,
14 pub address: String,
16 pub port: u16,
18 pub request_logging: bool,
20 pub metrics_enabled: bool,
22 pub default_timeout: Duration,
24 pub max_body_size: usize,
26 pub cors_enabled: bool,
28 pub cors_origins: Vec<String>,
30}
31
32impl Default for ServiceConfig {
33 fn default() -> Self {
34 Self {
35 name: "rustkernels".to_string(),
36 version: env!("CARGO_PKG_VERSION").to_string(),
37 address: "0.0.0.0".to_string(),
38 port: 8080,
39 request_logging: true,
40 metrics_enabled: true,
41 default_timeout: Duration::from_secs(30),
42 max_body_size: 10 * 1024 * 1024, cors_enabled: true,
44 cors_origins: vec!["*".to_string()],
45 }
46 }
47}
48
49impl ServiceConfig {
50 pub fn development() -> Self {
52 Self {
53 name: "rustkernels-dev".to_string(),
54 address: "127.0.0.1".to_string(),
55 request_logging: true,
56 ..Default::default()
57 }
58 }
59
60 pub fn production() -> Self {
62 Self {
63 request_logging: false, cors_origins: vec![], ..Default::default()
66 }
67 }
68
69 pub fn bind_address(&self) -> String {
71 format!("{}:{}", self.address, self.port)
72 }
73}
74
75#[derive(Debug, Clone)]
77pub struct RequestContext {
78 pub request_id: String,
80 pub start_time: Instant,
82 pub trace_id: Option<String>,
84 pub span_id: Option<String>,
86 pub tenant_id: Option<String>,
88 pub user_id: Option<String>,
90 pub path: String,
92 pub method: String,
94}
95
96impl RequestContext {
97 pub fn new(path: impl Into<String>, method: impl Into<String>) -> Self {
99 Self {
100 request_id: uuid::Uuid::new_v4().to_string(),
101 start_time: Instant::now(),
102 trace_id: None,
103 span_id: None,
104 tenant_id: None,
105 user_id: None,
106 path: path.into(),
107 method: method.into(),
108 }
109 }
110
111 pub fn with_trace_id(mut self, trace_id: impl Into<String>) -> Self {
113 self.trace_id = Some(trace_id.into());
114 self
115 }
116
117 pub fn with_tenant_id(mut self, tenant_id: impl Into<String>) -> Self {
119 self.tenant_id = Some(tenant_id.into());
120 self
121 }
122
123 pub fn with_user_id(mut self, user_id: impl Into<String>) -> Self {
125 self.user_id = Some(user_id.into());
126 self
127 }
128
129 pub fn elapsed(&self) -> Duration {
131 self.start_time.elapsed()
132 }
133
134 pub fn elapsed_us(&self) -> u64 {
136 self.start_time.elapsed().as_micros() as u64
137 }
138}
139
140#[derive(Debug, Clone, Serialize, Deserialize)]
142pub struct RateLimitConfig {
143 pub enabled: bool,
145 pub requests_per_second: u32,
147 pub burst_size: u32,
149 pub per_tenant: bool,
151}
152
153impl Default for RateLimitConfig {
154 fn default() -> Self {
155 Self {
156 enabled: true,
157 requests_per_second: 100,
158 burst_size: 200,
159 per_tenant: true,
160 }
161 }
162}
163
164pub struct ServiceMetrics {
166 pub total_requests: std::sync::atomic::AtomicU64,
168 pub total_errors: std::sync::atomic::AtomicU64,
170 pub total_latency_us: std::sync::atomic::AtomicU64,
172}
173
174impl ServiceMetrics {
175 pub fn new() -> Arc<Self> {
177 Arc::new(Self {
178 total_requests: std::sync::atomic::AtomicU64::new(0),
179 total_errors: std::sync::atomic::AtomicU64::new(0),
180 total_latency_us: std::sync::atomic::AtomicU64::new(0),
181 })
182 }
183
184 pub fn record_request(&self, latency_us: u64, is_error: bool) {
186 use std::sync::atomic::Ordering;
187 self.total_requests.fetch_add(1, Ordering::Relaxed);
188 self.total_latency_us
189 .fetch_add(latency_us, Ordering::Relaxed);
190 if is_error {
191 self.total_errors.fetch_add(1, Ordering::Relaxed);
192 }
193 }
194
195 pub fn request_count(&self) -> u64 {
197 self.total_requests
198 .load(std::sync::atomic::Ordering::Relaxed)
199 }
200
201 pub fn error_count(&self) -> u64 {
203 self.total_errors.load(std::sync::atomic::Ordering::Relaxed)
204 }
205
206 pub fn avg_latency_us(&self) -> f64 {
208 use std::sync::atomic::Ordering;
209 let total = self.total_latency_us.load(Ordering::Relaxed) as f64;
210 let count = self.total_requests.load(Ordering::Relaxed) as f64;
211 if count > 0.0 { total / count } else { 0.0 }
212 }
213}
214
215impl Default for ServiceMetrics {
216 fn default() -> Self {
217 Self {
218 total_requests: std::sync::atomic::AtomicU64::new(0),
219 total_errors: std::sync::atomic::AtomicU64::new(0),
220 total_latency_us: std::sync::atomic::AtomicU64::new(0),
221 }
222 }
223}
224
225pub mod paths {
227 pub const HEALTH: &str = "/health";
229 pub const LIVENESS: &str = "/health/live";
231 pub const READINESS: &str = "/health/ready";
233 pub const METRICS: &str = "/metrics";
235 pub const KERNEL_EXECUTE: &str = "/api/v1/kernels/:kernel_id/execute";
237 pub const KERNEL_LIST: &str = "/api/v1/kernels";
239 pub const KERNEL_INFO: &str = "/api/v1/kernels/:kernel_id";
241}
242
243pub mod headers {
245 pub const X_REQUEST_ID: &str = "X-Request-ID";
247 pub const TRACEPARENT: &str = "traceparent";
249 pub const X_TENANT_ID: &str = "X-Tenant-ID";
251 pub const X_API_KEY: &str = "X-API-Key";
253}
254
255#[cfg(test)]
256mod tests {
257 use super::*;
258
259 #[test]
260 fn test_service_config() {
261 let config = ServiceConfig::default();
262 assert_eq!(config.bind_address(), "0.0.0.0:8080");
263 }
264
265 #[test]
266 fn test_request_context() {
267 let ctx = RequestContext::new("/api/v1/kernels", "POST")
268 .with_tenant_id("tenant-123")
269 .with_user_id("user-456");
270
271 assert!(!ctx.request_id.is_empty());
272 assert_eq!(ctx.tenant_id, Some("tenant-123".to_string()));
273 assert_eq!(ctx.user_id, Some("user-456".to_string()));
274 }
275
276 #[test]
277 fn test_service_metrics() {
278 let metrics = ServiceMetrics::new();
279
280 metrics.record_request(1000, false);
281 metrics.record_request(2000, false);
282 metrics.record_request(3000, true);
283
284 assert_eq!(metrics.request_count(), 3);
285 assert_eq!(metrics.error_count(), 1);
286 assert!((metrics.avg_latency_us() - 2000.0).abs() < 0.1);
287 }
288}