Skip to main content

axon/
server_config.rs

1//! Server Config API — runtime-adjustable configuration for AxonServer.
2//!
3//! Provides a unified view of all configurable parameters and allows
4//! runtime updates via `GET/PUT /v1/config`. Changes take effect immediately
5//! without server restart.
6//!
7//! Configurable sections:
8//!   - `rate_limit` — max_requests, window_secs, enabled
9//!   - `request_log` — capacity, enabled
10//!   - `auth` — enabled (read-only; reflects api_keys state)
11//!
12//! The config snapshot is a serializable struct that captures the current state.
13//! Updates are partial: only fields present in the request are changed.
14
15use serde::{Deserialize, Serialize};
16
17// ── Config snapshot ────────────────────────────────────────────��────────
18
19/// Complete server configuration snapshot.
20#[derive(Debug, Clone, Serialize, Deserialize)]
21pub struct ConfigSnapshot {
22    pub rate_limit: RateLimitSection,
23    pub request_log: RequestLogSection,
24    pub auth: AuthSection,
25}
26
27/// Rate limiter configuration section.
28#[derive(Debug, Clone, Serialize, Deserialize)]
29pub struct RateLimitSection {
30    pub max_requests: u32,
31    pub window_secs: u64,
32    pub enabled: bool,
33}
34
35/// Request logger configuration section.
36#[derive(Debug, Clone, Serialize, Deserialize)]
37pub struct RequestLogSection {
38    pub capacity: usize,
39    pub enabled: bool,
40}
41
42/// Auth configuration section (read-only snapshot).
43#[derive(Debug, Clone, Serialize, Deserialize)]
44pub struct AuthSection {
45    pub enabled: bool,
46    pub active_keys: usize,
47    pub total_keys: usize,
48}
49
50// ── Config update (partial) ─────────────────────────────────────────────
51
52/// Partial configuration update request.
53/// Only present fields are applied.
54#[derive(Debug, Clone, Serialize, Deserialize, Default)]
55pub struct ConfigUpdate {
56    pub rate_limit: Option<RateLimitUpdate>,
57    pub request_log: Option<RequestLogUpdate>,
58}
59
60/// Partial rate limiter update.
61#[derive(Debug, Clone, Serialize, Deserialize, Default)]
62pub struct RateLimitUpdate {
63    pub max_requests: Option<u32>,
64    pub window_secs: Option<u64>,
65    pub enabled: Option<bool>,
66}
67
68/// Partial request logger update.
69#[derive(Debug, Clone, Serialize, Deserialize, Default)]
70pub struct RequestLogUpdate {
71    pub capacity: Option<usize>,
72    pub enabled: Option<bool>,
73}
74
75// ── Change tracking ─────────────────────────────────────────────────────
76
77/// A single config change that was applied.
78#[derive(Debug, Clone, Serialize)]
79pub struct ConfigChange {
80    pub section: String,
81    pub field: String,
82    pub old_value: String,
83    pub new_value: String,
84}
85
86/// Result of applying a config update.
87#[derive(Debug, Clone, Serialize)]
88pub struct ConfigUpdateResult {
89    pub applied: bool,
90    pub changes: Vec<ConfigChange>,
91    pub snapshot: ConfigSnapshot,
92}
93
94// ── Snapshot builder ────────────────────────────────────────────────────
95
96/// Build a ConfigSnapshot from the current server state components.
97pub fn snapshot(
98    rate_limiter: &crate::rate_limiter::RateLimiter,
99    request_logger: &crate::request_log::RequestLogger,
100    api_keys: &crate::api_keys::ApiKeyManager,
101) -> ConfigSnapshot {
102    let rl = rate_limiter.config();
103    let log = request_logger.config();
104
105    ConfigSnapshot {
106        rate_limit: RateLimitSection {
107            max_requests: rl.max_requests,
108            window_secs: rl.window.as_secs(),
109            enabled: rl.enabled,
110        },
111        request_log: RequestLogSection {
112            capacity: log.capacity,
113            enabled: log.enabled,
114        },
115        auth: AuthSection {
116            enabled: api_keys.is_enabled(),
117            active_keys: api_keys.active_count(),
118            total_keys: api_keys.total_count(),
119        },
120    }
121}
122
123/// Build a ConfigSnapshot from rate_limiter, request_logger, and a pre-extracted AuthSection.
124pub fn snapshot_with_auth(
125    rate_limiter: &crate::rate_limiter::RateLimiter,
126    request_logger: &crate::request_log::RequestLogger,
127    auth: &AuthSection,
128) -> ConfigSnapshot {
129    let rl = rate_limiter.config();
130    let log = request_logger.config();
131
132    ConfigSnapshot {
133        rate_limit: RateLimitSection {
134            max_requests: rl.max_requests,
135            window_secs: rl.window.as_secs(),
136            enabled: rl.enabled,
137        },
138        request_log: RequestLogSection {
139            capacity: log.capacity,
140            enabled: log.enabled,
141        },
142        auth: auth.clone(),
143    }
144}
145
146// ── Apply update ────────────────────────────────────────────────────────
147
148/// Apply rate_limit portion of a config update. Returns changes.
149pub fn apply_rate_limit(
150    update: &RateLimitUpdate,
151    rate_limiter: &mut crate::rate_limiter::RateLimiter,
152) -> Vec<ConfigChange> {
153    let mut changes = Vec::new();
154    let old = rate_limiter.config().clone();
155
156    if let Some(max) = update.max_requests {
157        if max != old.max_requests {
158            changes.push(ConfigChange {
159                section: "rate_limit".into(),
160                field: "max_requests".into(),
161                old_value: old.max_requests.to_string(),
162                new_value: max.to_string(),
163            });
164        }
165    }
166    if let Some(secs) = update.window_secs {
167        if secs != old.window.as_secs() {
168            changes.push(ConfigChange {
169                section: "rate_limit".into(),
170                field: "window_secs".into(),
171                old_value: old.window.as_secs().to_string(),
172                new_value: secs.to_string(),
173            });
174        }
175    }
176    if let Some(en) = update.enabled {
177        if en != old.enabled {
178            changes.push(ConfigChange {
179                section: "rate_limit".into(),
180                field: "enabled".into(),
181                old_value: old.enabled.to_string(),
182                new_value: en.to_string(),
183            });
184        }
185    }
186
187    rate_limiter.update_config(update.max_requests, update.window_secs, update.enabled);
188    changes
189}
190
191/// Apply request_log portion of a config update. Returns changes.
192pub fn apply_request_log(
193    update: &RequestLogUpdate,
194    request_logger: &mut crate::request_log::RequestLogger,
195) -> Vec<ConfigChange> {
196    let mut changes = Vec::new();
197    let old = request_logger.config().clone();
198
199    if let Some(cap) = update.capacity {
200        if cap != old.capacity {
201            changes.push(ConfigChange {
202                section: "request_log".into(),
203                field: "capacity".into(),
204                old_value: old.capacity.to_string(),
205                new_value: cap.to_string(),
206            });
207        }
208    }
209    if let Some(en) = update.enabled {
210        if en != old.enabled {
211            changes.push(ConfigChange {
212                section: "request_log".into(),
213                field: "enabled".into(),
214                old_value: old.enabled.to_string(),
215                new_value: en.to_string(),
216            });
217        }
218    }
219
220    request_logger.update_config(update.capacity, update.enabled);
221    changes
222}
223
224/// Apply a partial config update. Dispatches to per-section apply functions.
225/// Returns a result with the list of changes and the new snapshot.
226pub fn apply(
227    update: &ConfigUpdate,
228    rate_limiter: &mut crate::rate_limiter::RateLimiter,
229    request_logger: &mut crate::request_log::RequestLogger,
230    auth_snap: &AuthSection,
231) -> ConfigUpdateResult {
232    let mut changes = Vec::new();
233
234    if let Some(ref rl) = update.rate_limit {
235        changes.extend(apply_rate_limit(rl, rate_limiter));
236    }
237    if let Some(ref log) = update.request_log {
238        changes.extend(apply_request_log(log, request_logger));
239    }
240
241    let new_snapshot = snapshot_with_auth(rate_limiter, request_logger, auth_snap);
242
243    ConfigUpdateResult {
244        applied: !changes.is_empty(),
245        changes,
246        snapshot: new_snapshot,
247    }
248}
249
250// ── Tests ────────────────────────────────────────────────────────────────
251
252#[cfg(test)]
253mod tests {
254    use super::*;
255    use crate::api_keys::ApiKeyManager;
256    use crate::rate_limiter::{RateLimiter, RateLimitConfig};
257    use crate::request_log::{RequestLogger, RequestLogConfig};
258
259    fn make_components() -> (RateLimiter, RequestLogger, AuthSection) {
260        let rl = RateLimiter::new(RateLimitConfig::default_config());
261        let log = RequestLogger::new(RequestLogConfig::default_config());
262        let auth = AuthSection { enabled: false, active_keys: 0, total_keys: 0 };
263        (rl, log, auth)
264    }
265
266    #[test]
267    fn snapshot_captures_defaults() {
268        let (rl, log, auth) = make_components();
269        let snap = snapshot_with_auth(&rl, &log, &auth);
270
271        assert_eq!(snap.rate_limit.max_requests, 100);
272        assert_eq!(snap.rate_limit.window_secs, 60);
273        assert!(snap.rate_limit.enabled);
274        assert_eq!(snap.request_log.capacity, 1000);
275        assert!(snap.request_log.enabled);
276        assert!(!snap.auth.enabled);
277        assert_eq!(snap.auth.active_keys, 0);
278    }
279
280    #[test]
281    fn snapshot_with_auth_enabled() {
282        let rl = RateLimiter::new(RateLimitConfig::default_config());
283        let log = RequestLogger::new(RequestLogConfig::default_config());
284        let keys = ApiKeyManager::new(Some("master_tok"));
285        let auth = AuthSection {
286            enabled: keys.is_enabled(),
287            active_keys: keys.active_count(),
288            total_keys: keys.total_count(),
289        };
290
291        let snap = snapshot_with_auth(&rl, &log, &auth);
292        assert!(snap.auth.enabled);
293        assert_eq!(snap.auth.active_keys, 1);
294        assert_eq!(snap.auth.total_keys, 1);
295    }
296
297    #[test]
298    fn apply_rate_limit_changes() {
299        let (mut rl, mut log, auth) = make_components();
300
301        let update = ConfigUpdate {
302            rate_limit: Some(RateLimitUpdate {
303                max_requests: Some(200),
304                window_secs: Some(120),
305                enabled: None,
306            }),
307            request_log: None,
308        };
309
310        let result = apply(&update, &mut rl, &mut log, &auth);
311        assert!(result.applied);
312        assert_eq!(result.changes.len(), 2);
313        assert_eq!(result.snapshot.rate_limit.max_requests, 200);
314        assert_eq!(result.snapshot.rate_limit.window_secs, 120);
315        assert!(result.snapshot.rate_limit.enabled); // unchanged
316    }
317
318    #[test]
319    fn apply_request_log_changes() {
320        let (mut rl, mut log, auth) = make_components();
321
322        let update = ConfigUpdate {
323            rate_limit: None,
324            request_log: Some(RequestLogUpdate {
325                capacity: Some(500),
326                enabled: Some(false),
327            }),
328        };
329
330        let result = apply(&update, &mut rl, &mut log, &auth);
331        assert!(result.applied);
332        assert_eq!(result.changes.len(), 2);
333        assert_eq!(result.snapshot.request_log.capacity, 500);
334        assert!(!result.snapshot.request_log.enabled);
335    }
336
337    #[test]
338    fn apply_no_changes_when_same_values() {
339        let (mut rl, mut log, auth) = make_components();
340
341        let update = ConfigUpdate {
342            rate_limit: Some(RateLimitUpdate {
343                max_requests: Some(100), // same as default
344                window_secs: Some(60),   // same as default
345                enabled: None,
346            }),
347            request_log: None,
348        };
349
350        let result = apply(&update, &mut rl, &mut log, &auth);
351        assert!(!result.applied);
352        assert!(result.changes.is_empty());
353    }
354
355    #[test]
356    fn apply_empty_update() {
357        let (mut rl, mut log, auth) = make_components();
358
359        let update = ConfigUpdate::default();
360        let result = apply(&update, &mut rl, &mut log, &auth);
361        assert!(!result.applied);
362        assert!(result.changes.is_empty());
363    }
364
365    #[test]
366    fn apply_combined_changes() {
367        let (mut rl, mut log, auth) = make_components();
368
369        let update = ConfigUpdate {
370            rate_limit: Some(RateLimitUpdate {
371                max_requests: Some(50),
372                window_secs: None,
373                enabled: Some(false),
374            }),
375            request_log: Some(RequestLogUpdate {
376                capacity: Some(2000),
377                enabled: None,
378            }),
379        };
380
381        let result = apply(&update, &mut rl, &mut log, &auth);
382        assert!(result.applied);
383        assert_eq!(result.changes.len(), 3);
384        assert_eq!(result.snapshot.rate_limit.max_requests, 50);
385        assert!(!result.snapshot.rate_limit.enabled);
386        assert_eq!(result.snapshot.request_log.capacity, 2000);
387    }
388
389    #[test]
390    fn change_tracking_records_old_and_new() {
391        let (mut rl, mut log, auth) = make_components();
392
393        let update = ConfigUpdate {
394            rate_limit: Some(RateLimitUpdate {
395                max_requests: Some(250),
396                window_secs: None,
397                enabled: None,
398            }),
399            request_log: None,
400        };
401
402        let result = apply(&update, &mut rl, &mut log, &auth);
403        assert_eq!(result.changes.len(), 1);
404        let c = &result.changes[0];
405        assert_eq!(c.section, "rate_limit");
406        assert_eq!(c.field, "max_requests");
407        assert_eq!(c.old_value, "100");
408        assert_eq!(c.new_value, "250");
409    }
410
411    #[test]
412    fn snapshot_serializes_to_json() {
413        let (rl, log, auth) = make_components();
414        let snap = snapshot_with_auth(&rl, &log, &auth);
415        let json = serde_json::to_value(&snap).unwrap();
416
417        assert_eq!(json["rate_limit"]["max_requests"], 100);
418        assert_eq!(json["rate_limit"]["window_secs"], 60);
419        assert_eq!(json["request_log"]["capacity"], 1000);
420        assert_eq!(json["auth"]["enabled"], false);
421    }
422
423    #[test]
424    fn update_result_serializes() {
425        let (mut rl, mut log, auth) = make_components();
426        let update = ConfigUpdate {
427            rate_limit: Some(RateLimitUpdate {
428                max_requests: Some(75),
429                window_secs: None,
430                enabled: None,
431            }),
432            request_log: None,
433        };
434
435        let result = apply(&update, &mut rl, &mut log, &auth);
436        let json = serde_json::to_value(&result).unwrap();
437        assert_eq!(json["applied"], true);
438        assert!(json["changes"].as_array().unwrap().len() == 1);
439        assert_eq!(json["snapshot"]["rate_limit"]["max_requests"], 75);
440    }
441
442    #[test]
443    fn disable_then_reenable_rate_limit() {
444        let (mut rl, mut log, auth) = make_components();
445
446        // Disable
447        let update = ConfigUpdate {
448            rate_limit: Some(RateLimitUpdate {
449                enabled: Some(false),
450                ..Default::default()
451            }),
452            request_log: None,
453        };
454        let result = apply(&update, &mut rl, &mut log, &auth);
455        assert!(!result.snapshot.rate_limit.enabled);
456
457        // Re-enable with new limit
458        let update = ConfigUpdate {
459            rate_limit: Some(RateLimitUpdate {
460                enabled: Some(true),
461                max_requests: Some(500),
462                ..Default::default()
463            }),
464            request_log: None,
465        };
466        let result = apply(&update, &mut rl, &mut log, &auth);
467        assert!(result.snapshot.rate_limit.enabled);
468        assert_eq!(result.snapshot.rate_limit.max_requests, 500);
469    }
470}