dynamo_runtime/
config.rs

1// SPDX-FileCopyrightText: Copyright (c) 2024-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
2// SPDX-License-Identifier: Apache-2.0
3
4use anyhow::Result;
5use derive_builder::Builder;
6use figment::{
7    Figment,
8    providers::{Env, Format, Serialized, Toml},
9};
10use serde::{Deserialize, Serialize};
11use std::fmt;
12use std::sync::OnceLock;
13use validator::Validate;
14
15pub mod environment_names;
16
17/// Default system host for health and metrics endpoints
18const DEFAULT_SYSTEM_HOST: &str = "0.0.0.0";
19
20/// Default system port for health and metrics endpoints (-1 = disabled)
21const DEFAULT_SYSTEM_PORT: i16 = -1;
22
23/// Default health endpoint paths
24const DEFAULT_SYSTEM_HEALTH_PATH: &str = "/health";
25const DEFAULT_SYSTEM_LIVE_PATH: &str = "/live";
26
27/// Default health check configuration
28/// This is the wait time before sending canary health checks when no activity is detected
29pub const DEFAULT_CANARY_WAIT_TIME_SECS: u64 = 10;
30/// Default timeout for individual health check requests
31pub const DEFAULT_HEALTH_CHECK_REQUEST_TIMEOUT_SECS: u64 = 3;
32
33#[derive(Debug, Clone, Serialize, Deserialize)]
34pub struct WorkerConfig {
35    /// Grace shutdown period for the system server.
36    pub graceful_shutdown_timeout: u64,
37}
38
39impl WorkerConfig {
40    /// Instantiates and reads server configurations from appropriate sources.
41    /// Panics on invalid configuration.
42    pub fn from_settings() -> Self {
43        // All calls should be global and thread safe.
44        Figment::new()
45            .merge(Serialized::defaults(Self::default()))
46            .merge(Env::prefixed("DYN_WORKER_"))
47            .extract()
48            .unwrap() // safety: Called on startup, so panic is reasonable
49    }
50}
51
52impl Default for WorkerConfig {
53    fn default() -> Self {
54        WorkerConfig {
55            graceful_shutdown_timeout: if cfg!(debug_assertions) {
56                1 // Debug build: 1 second
57            } else {
58                30 // Release build: 30 seconds
59            },
60        }
61    }
62}
63
64#[derive(Debug, Deserialize, Serialize, PartialEq, Clone)]
65#[serde(rename_all = "lowercase")]
66pub enum HealthStatus {
67    Ready,
68    NotReady,
69}
70
71/// Runtime configuration
72/// Defines the configuration for Tokio runtimes
73#[derive(Serialize, Deserialize, Validate, Debug, Builder, Clone)]
74#[builder(build_fn(private, name = "build_internal"), derive(Debug, Serialize))]
75pub struct RuntimeConfig {
76    /// Number of async worker threads
77    /// If set to 1, the runtime will run in single-threaded mode
78    /// Set this at runtime with environment variable DYN_RUNTIME_NUM_WORKER_THREADS. Defaults to
79    /// number of cores.
80    #[validate(range(min = 1))]
81    #[builder_field_attr(serde(skip_serializing_if = "Option::is_none"))]
82    pub num_worker_threads: Option<usize>,
83
84    /// Maximum number of blocking threads
85    /// Blocking threads are used for blocking operations, this value must be greater than 0.
86    /// Set this at runtime with environment variable DYN_RUNTIME_MAX_BLOCKING_THREADS. Defaults to
87    /// 512.
88    #[validate(range(min = 1))]
89    #[builder(default = "512")]
90    #[builder_field_attr(serde(skip_serializing_if = "Option::is_none"))]
91    pub max_blocking_threads: usize,
92
93    /// System status server host for health and metrics endpoints
94    /// Set this at runtime with environment variable DYN_SYSTEM_HOST
95    #[builder(default = "DEFAULT_SYSTEM_HOST.to_string()")]
96    #[builder_field_attr(serde(skip_serializing_if = "Option::is_none"))]
97    pub system_host: String,
98
99    /// System status server port for health and metrics endpoints
100    /// Set to -1 to disable the system status server (default)
101    /// Set to 0 to bind to a random available port
102    /// Set to a positive port number (e.g. 8081) to bind to a specific port
103    /// Set this at runtime with environment variable DYN_SYSTEM_PORT
104    #[builder(default = "DEFAULT_SYSTEM_PORT")]
105    #[builder_field_attr(serde(skip_serializing_if = "Option::is_none"))]
106    pub system_port: i16,
107
108    /// Health and metrics System status server enabled (DEPRECATED)
109    /// This field is deprecated. Use system_port instead (set to positive value to enable)
110    /// Environment variable DYN_SYSTEM_ENABLED is deprecated
111    #[deprecated(
112        note = "Use system_port instead. Set DYN_SYSTEM_PORT to enable the system metrics server."
113    )]
114    #[builder(default = "false")]
115    #[builder_field_attr(serde(skip_serializing_if = "Option::is_none"))]
116    pub system_enabled: bool,
117
118    /// Starting Health Status
119    /// Set this at runtime with environment variable DYN_SYSTEM_STARTING_HEALTH_STATUS
120    #[builder(default = "HealthStatus::NotReady")]
121    #[builder_field_attr(serde(skip_serializing_if = "Option::is_none"))]
122    pub starting_health_status: HealthStatus,
123
124    /// Use Endpoint Health Status
125    /// When using endpoint health status, health status
126    /// is the AND of individual endpoint health
127    /// Set this at runtime with environment variable DYN_SYSTEM_USE_ENDPOINT_HEALTH_STATUS
128    /// with the list of endpoints to consider for system health
129    #[builder(default = "vec![]")]
130    #[builder_field_attr(serde(skip_serializing_if = "Option::is_none"))]
131    pub use_endpoint_health_status: Vec<String>,
132
133    /// Health endpoint paths
134    /// Set this at runtime with environment variable DYN_SYSTEM_HEALTH_PATH
135    #[builder(default = "DEFAULT_SYSTEM_HEALTH_PATH.to_string()")]
136    #[builder_field_attr(serde(skip_serializing_if = "Option::is_none"))]
137    pub system_health_path: String,
138    /// Set this at runtime with environment variable DYN_SYSTEM_LIVE_PATH
139    #[builder(default = "DEFAULT_SYSTEM_LIVE_PATH.to_string()")]
140    #[builder_field_attr(serde(skip_serializing_if = "Option::is_none"))]
141    pub system_live_path: String,
142
143    /// Number of threads for the Rayon compute pool
144    /// If not set, defaults to num_cpus / 2
145    /// Set this at runtime with environment variable DYN_COMPUTE_THREADS
146    #[builder(default = "None")]
147    #[builder_field_attr(serde(skip_serializing_if = "Option::is_none"))]
148    pub compute_threads: Option<usize>,
149
150    /// Stack size for compute threads in bytes
151    /// Defaults to 2MB (2097152 bytes)
152    /// Set this at runtime with environment variable DYN_COMPUTE_STACK_SIZE
153    #[builder(default = "Some(2 * 1024 * 1024)")]
154    #[builder_field_attr(serde(skip_serializing_if = "Option::is_none"))]
155    pub compute_stack_size: Option<usize>,
156
157    /// Thread name prefix for compute pool threads
158    /// Set this at runtime with environment variable DYN_COMPUTE_THREAD_PREFIX
159    #[builder(default = "\"compute\".to_string()")]
160    #[builder_field_attr(serde(skip_serializing_if = "Option::is_none"))]
161    pub compute_thread_prefix: String,
162
163    /// Enable active health checking with payloads
164    /// Set this at runtime with environment variable DYN_HEALTH_CHECK_ENABLED
165    #[builder(default = "false")]
166    #[builder_field_attr(serde(skip_serializing_if = "Option::is_none"))]
167    pub health_check_enabled: bool,
168
169    /// Canary wait time in seconds (time to wait before sending health check when no activity)
170    /// Set this at runtime with environment variable DYN_CANARY_WAIT_TIME
171    #[builder(default = "DEFAULT_CANARY_WAIT_TIME_SECS")]
172    #[builder_field_attr(serde(skip_serializing_if = "Option::is_none"))]
173    pub canary_wait_time_secs: u64,
174
175    /// Health check request timeout in seconds
176    /// Set this at runtime with environment variable DYN_HEALTH_CHECK_REQUEST_TIMEOUT
177    #[builder(default = "DEFAULT_HEALTH_CHECK_REQUEST_TIMEOUT_SECS")]
178    #[builder_field_attr(serde(skip_serializing_if = "Option::is_none"))]
179    pub health_check_request_timeout_secs: u64,
180}
181
182impl fmt::Display for RuntimeConfig {
183    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
184        // If None, it defaults to "number of cores", so we indicate that.
185        match self.num_worker_threads {
186            Some(val) => write!(f, "num_worker_threads={val}, ")?,
187            None => write!(f, "num_worker_threads=default (num_cores), ")?,
188        }
189
190        write!(f, "max_blocking_threads={}, ", self.max_blocking_threads)?;
191        write!(f, "system_host={}, ", self.system_host)?;
192        write!(f, "system_port={}, ", self.system_port)?;
193        write!(
194            f,
195            "use_endpoint_health_status={:?}",
196            self.use_endpoint_health_status
197        )?;
198        write!(
199            f,
200            "starting_health_status={:?}",
201            self.starting_health_status
202        )?;
203        write!(f, ", system_health_path={}", self.system_health_path)?;
204        write!(f, ", system_live_path={}", self.system_live_path)?;
205        write!(f, ", health_check_enabled={}", self.health_check_enabled)?;
206        write!(f, ", canary_wait_time_secs={}", self.canary_wait_time_secs)?;
207        write!(
208            f,
209            ", health_check_request_timeout_secs={}",
210            self.health_check_request_timeout_secs
211        )?;
212
213        Ok(())
214    }
215}
216
217impl RuntimeConfig {
218    pub fn builder() -> RuntimeConfigBuilder {
219        RuntimeConfigBuilder::default()
220    }
221
222    pub(crate) fn figment() -> Figment {
223        Figment::new()
224            .merge(Serialized::defaults(RuntimeConfig::default()))
225            .merge(Toml::file("/opt/dynamo/defaults/runtime.toml"))
226            .merge(Toml::file("/opt/dynamo/etc/runtime.toml"))
227            .merge(Env::prefixed("DYN_RUNTIME_").filter_map(|k| {
228                let full_key = format!("DYN_RUNTIME_{}", k.as_str());
229                // filters out empty environment variables
230                match std::env::var(&full_key) {
231                    Ok(v) if !v.is_empty() => Some(k.into()),
232                    _ => None,
233                }
234            }))
235            .merge(Env::prefixed("DYN_SYSTEM_").filter_map(|k| {
236                let full_key = format!("DYN_SYSTEM_{}", k.as_str());
237                // filters out empty environment variables
238                match std::env::var(&full_key) {
239                    Ok(v) if !v.is_empty() => {
240                        // Map DYN_SYSTEM_* to the correct field names
241                        let mapped_key = match k.as_str() {
242                            "HOST" => "system_host",
243                            "PORT" => "system_port",
244                            "ENABLED" => "system_enabled",
245                            "USE_ENDPOINT_HEALTH_STATUS" => "use_endpoint_health_status",
246                            "STARTING_HEALTH_STATUS" => "starting_health_status",
247                            "HEALTH_PATH" => "system_health_path",
248                            "LIVE_PATH" => "system_live_path",
249                            _ => k.as_str(),
250                        };
251                        Some(mapped_key.into())
252                    }
253                    _ => None,
254                }
255            }))
256            .merge(Env::prefixed("DYN_COMPUTE_").filter_map(|k| {
257                let full_key = format!("DYN_COMPUTE_{}", k.as_str());
258                // filters out empty environment variables
259                match std::env::var(&full_key) {
260                    Ok(v) if !v.is_empty() => {
261                        // Map DYN_COMPUTE_* to the correct field names
262                        let mapped_key = match k.as_str() {
263                            "THREADS" => "compute_threads",
264                            "STACK_SIZE" => "compute_stack_size",
265                            "THREAD_PREFIX" => "compute_thread_prefix",
266                            _ => k.as_str(),
267                        };
268                        Some(mapped_key.into())
269                    }
270                    _ => None,
271                }
272            }))
273            .merge(Env::prefixed("DYN_HEALTH_CHECK_").filter_map(|k| {
274                let full_key = format!("DYN_HEALTH_CHECK_{}", k.as_str());
275                // filters out empty environment variables
276                match std::env::var(&full_key) {
277                    Ok(v) if !v.is_empty() => {
278                        // Map DYN_HEALTH_CHECK_* to the correct field names
279                        let mapped_key = match k.as_str() {
280                            "ENABLED" => "health_check_enabled",
281                            "REQUEST_TIMEOUT" => "health_check_request_timeout_secs",
282                            _ => k.as_str(),
283                        };
284                        Some(mapped_key.into())
285                    }
286                    _ => None,
287                }
288            }))
289            .merge(Env::prefixed("DYN_CANARY_").filter_map(|k| {
290                let full_key = format!("DYN_CANARY_{}", k.as_str());
291                // filters out empty environment variables
292                match std::env::var(&full_key) {
293                    Ok(v) if !v.is_empty() => {
294                        // Map DYN_CANARY_* to the correct field names
295                        let mapped_key = match k.as_str() {
296                            "WAIT_TIME" => "canary_wait_time_secs",
297                            _ => k.as_str(),
298                        };
299                        Some(mapped_key.into())
300                    }
301                    _ => None,
302                }
303            }))
304    }
305
306    /// Load the runtime configuration from the environment and configuration files
307    /// Configuration is priorities in the following order, where the last has the lowest priority:
308    /// 1. Environment variables (top priority)
309    ///    TO DO: Add documentation for configuration files. Paths should be configurable.
310    /// 2. /opt/dynamo/etc/runtime.toml
311    /// 3. /opt/dynamo/defaults/runtime.toml (lowest priority)
312    ///
313    /// Environment variables are prefixed with `DYN_RUNTIME_` and `DYN_SYSTEM`
314    pub fn from_settings() -> Result<RuntimeConfig> {
315        use environment_names::runtime::system as env_system;
316        // Check for deprecated environment variables
317        if std::env::var(env_system::DYN_SYSTEM_USE_ENDPOINT_HEALTH_STATUS).is_ok() {
318            tracing::warn!(
319                "DYN_SYSTEM_USE_ENDPOINT_HEALTH_STATUS is deprecated and no longer used. \
320                System health is now determined by endpoints that register with health check payloads. \
321                Please update your configuration to register health check payloads directly on endpoints."
322            );
323        }
324
325        if std::env::var(env_system::DYN_SYSTEM_ENABLED).is_ok() {
326            tracing::warn!(
327                "DYN_SYSTEM_ENABLED is deprecated. \
328                System metrics server is now controlled solely by DYN_SYSTEM_PORT. \
329                Set DYN_SYSTEM_PORT to a positive value to enable the server, or set to -1 to disable (default)."
330            );
331        }
332
333        let config: RuntimeConfig = Self::figment().extract()?;
334        config.validate()?;
335        Ok(config)
336    }
337
338    /// Check if System server should be enabled
339    /// System server is enabled when DYN_SYSTEM_PORT is set to 0 or a positive value
340    /// Port 0 binds to a random available port
341    /// Negative values disable the server
342    pub fn system_server_enabled(&self) -> bool {
343        self.system_port >= 0
344    }
345
346    pub fn single_threaded() -> Self {
347        RuntimeConfig {
348            num_worker_threads: Some(1),
349            max_blocking_threads: 1,
350            system_host: DEFAULT_SYSTEM_HOST.to_string(),
351            system_port: DEFAULT_SYSTEM_PORT,
352            #[allow(deprecated)]
353            system_enabled: false,
354            starting_health_status: HealthStatus::NotReady,
355            use_endpoint_health_status: vec![],
356            system_health_path: DEFAULT_SYSTEM_HEALTH_PATH.to_string(),
357            system_live_path: DEFAULT_SYSTEM_LIVE_PATH.to_string(),
358            compute_threads: Some(1),
359            compute_stack_size: Some(2 * 1024 * 1024),
360            compute_thread_prefix: "compute".to_string(),
361            health_check_enabled: false,
362            canary_wait_time_secs: DEFAULT_CANARY_WAIT_TIME_SECS,
363            health_check_request_timeout_secs: DEFAULT_HEALTH_CHECK_REQUEST_TIMEOUT_SECS,
364        }
365    }
366
367    /// Create a new default runtime configuration
368    pub(crate) fn create_runtime(&self) -> std::io::Result<tokio::runtime::Runtime> {
369        tokio::runtime::Builder::new_multi_thread()
370            .worker_threads(
371                self.num_worker_threads
372                    .unwrap_or_else(|| std::thread::available_parallelism().unwrap().get()),
373            )
374            .max_blocking_threads(self.max_blocking_threads)
375            .enable_all()
376            .build()
377    }
378}
379
380impl Default for RuntimeConfig {
381    fn default() -> Self {
382        let num_cores = std::thread::available_parallelism().unwrap().get();
383        Self {
384            num_worker_threads: Some(num_cores),
385            max_blocking_threads: num_cores,
386            system_host: DEFAULT_SYSTEM_HOST.to_string(),
387            system_port: DEFAULT_SYSTEM_PORT,
388            #[allow(deprecated)]
389            system_enabled: false,
390            starting_health_status: HealthStatus::NotReady,
391            use_endpoint_health_status: vec![],
392            system_health_path: DEFAULT_SYSTEM_HEALTH_PATH.to_string(),
393            system_live_path: DEFAULT_SYSTEM_LIVE_PATH.to_string(),
394            compute_threads: None,
395            compute_stack_size: Some(2 * 1024 * 1024),
396            compute_thread_prefix: "compute".to_string(),
397            health_check_enabled: false,
398            canary_wait_time_secs: DEFAULT_CANARY_WAIT_TIME_SECS,
399            health_check_request_timeout_secs: DEFAULT_HEALTH_CHECK_REQUEST_TIMEOUT_SECS,
400        }
401    }
402}
403
404impl RuntimeConfigBuilder {
405    /// Build and validate the runtime configuration
406    pub fn build(&self) -> Result<RuntimeConfig> {
407        let config = self.build_internal()?;
408        config.validate()?;
409        Ok(config)
410    }
411}
412
413/// Check if a string is truthy
414/// This will be used to evaluate environment variables or any other subjective
415/// configuration parameters that can be set by the user that should be evaluated
416/// as a boolean value.
417pub fn is_truthy(val: &str) -> bool {
418    matches!(val.to_lowercase().as_str(), "1" | "true" | "on" | "yes")
419}
420
421pub fn parse_bool(val: &str) -> anyhow::Result<bool> {
422    if is_truthy(val) {
423        Ok(true)
424    } else if is_falsey(val) {
425        Ok(false)
426    } else {
427        anyhow::bail!(
428            "Invalid boolean value: '{}'. Expected one of: true/false, 1/0, on/off, yes/no",
429            val
430        )
431    }
432}
433
434/// Check if a string is falsey
435/// This will be used to evaluate environment variables or any other subjective
436/// configuration parameters that can be set by the user that should be evaluated
437/// as a boolean value (opposite of is_truthy).
438pub fn is_falsey(val: &str) -> bool {
439    matches!(val.to_lowercase().as_str(), "0" | "false" | "off" | "no")
440}
441
442/// Check if an environment variable is truthy
443pub fn env_is_truthy(env: &str) -> bool {
444    match std::env::var(env) {
445        Ok(val) => is_truthy(val.as_str()),
446        Err(_) => false,
447    }
448}
449
450/// Check if an environment variable is falsey
451pub fn env_is_falsey(env: &str) -> bool {
452    match std::env::var(env) {
453        Ok(val) => is_falsey(val.as_str()),
454        Err(_) => false,
455    }
456}
457
458/// Check whether JSONL logging enabled
459/// Set the `DYN_LOGGING_JSONL` environment variable a [`is_truthy`] value
460pub fn jsonl_logging_enabled() -> bool {
461    env_is_truthy(environment_names::logging::DYN_LOGGING_JSONL)
462}
463
464/// Check whether logging with ANSI terminal escape codes and colors is disabled.
465/// Set the `DYN_SDK_DISABLE_ANSI_LOGGING` environment variable a [`is_truthy`] value
466pub fn disable_ansi_logging() -> bool {
467    env_is_truthy(environment_names::logging::DYN_SDK_DISABLE_ANSI_LOGGING)
468}
469
470/// Check whether to use local timezone for logging timestamps (default is UTC)
471/// Set the `DYN_LOG_USE_LOCAL_TZ` environment variable to a [`is_truthy`] value
472pub fn use_local_timezone() -> bool {
473    env_is_truthy(environment_names::logging::DYN_LOG_USE_LOCAL_TZ)
474}
475
476#[cfg(test)]
477mod tests {
478    use super::*;
479
480    #[test]
481    fn test_runtime_config_with_env_vars() -> Result<()> {
482        use environment_names::runtime;
483        temp_env::with_vars(
484            vec![
485                (runtime::DYN_RUNTIME_NUM_WORKER_THREADS, Some("24")),
486                (runtime::DYN_RUNTIME_MAX_BLOCKING_THREADS, Some("32")),
487            ],
488            || {
489                let config = RuntimeConfig::from_settings()?;
490                assert_eq!(config.num_worker_threads, Some(24));
491                assert_eq!(config.max_blocking_threads, 32);
492                Ok(())
493            },
494        )
495    }
496
497    #[test]
498    fn test_runtime_config_defaults() -> Result<()> {
499        use environment_names::runtime;
500        temp_env::with_vars(
501            vec![
502                (runtime::DYN_RUNTIME_NUM_WORKER_THREADS, None::<&str>),
503                (runtime::DYN_RUNTIME_MAX_BLOCKING_THREADS, Some("")),
504            ],
505            || {
506                let config = RuntimeConfig::from_settings()?;
507
508                let default_config = RuntimeConfig::default();
509                assert_eq!(config.num_worker_threads, default_config.num_worker_threads);
510                assert_eq!(
511                    config.max_blocking_threads,
512                    default_config.max_blocking_threads
513                );
514                Ok(())
515            },
516        )
517    }
518
519    #[test]
520    fn test_runtime_config_rejects_invalid_thread_count() -> Result<()> {
521        use environment_names::runtime;
522        temp_env::with_vars(
523            vec![
524                (runtime::DYN_RUNTIME_NUM_WORKER_THREADS, Some("0")),
525                (runtime::DYN_RUNTIME_MAX_BLOCKING_THREADS, Some("0")),
526            ],
527            || {
528                let result = RuntimeConfig::from_settings();
529                assert!(result.is_err());
530                if let Err(e) = result {
531                    assert!(
532                        e.to_string()
533                            .contains("num_worker_threads: Validation error")
534                    );
535                    assert!(
536                        e.to_string()
537                            .contains("max_blocking_threads: Validation error")
538                    );
539                }
540                Ok(())
541            },
542        )
543    }
544
545    #[test]
546    fn test_runtime_config_system_server_env_vars() -> Result<()> {
547        use environment_names::runtime::system;
548        temp_env::with_vars(
549            vec![
550                (system::DYN_SYSTEM_HOST, Some("127.0.0.1")),
551                (system::DYN_SYSTEM_PORT, Some("9090")),
552            ],
553            || {
554                let config = RuntimeConfig::from_settings()?;
555                assert_eq!(config.system_host, "127.0.0.1");
556                assert_eq!(config.system_port, 9090);
557                Ok(())
558            },
559        )
560    }
561
562    #[test]
563    fn test_system_server_disabled_by_default() {
564        use environment_names::runtime::system;
565        temp_env::with_vars(vec![(system::DYN_SYSTEM_PORT, None::<&str>)], || {
566            let config = RuntimeConfig::from_settings().unwrap();
567            assert!(!config.system_server_enabled());
568            assert_eq!(config.system_port, -1);
569        });
570    }
571
572    #[test]
573    fn test_system_server_disabled_with_negative_port() {
574        use environment_names::runtime::system;
575        temp_env::with_vars(vec![(system::DYN_SYSTEM_PORT, Some("-1"))], || {
576            let config = RuntimeConfig::from_settings().unwrap();
577            assert!(!config.system_server_enabled());
578            assert_eq!(config.system_port, -1);
579        });
580    }
581
582    #[test]
583    fn test_system_server_enabled_with_port() {
584        use environment_names::runtime::system;
585        temp_env::with_vars(vec![(system::DYN_SYSTEM_PORT, Some("9527"))], || {
586            let config = RuntimeConfig::from_settings().unwrap();
587            assert!(config.system_server_enabled());
588            assert_eq!(config.system_port, 9527);
589        });
590    }
591
592    #[test]
593    fn test_system_server_starting_health_status_ready() {
594        use environment_names::runtime::system;
595        temp_env::with_vars(
596            vec![(system::DYN_SYSTEM_STARTING_HEALTH_STATUS, Some("ready"))],
597            || {
598                let config = RuntimeConfig::from_settings().unwrap();
599                assert!(config.starting_health_status == HealthStatus::Ready);
600            },
601        );
602    }
603
604    #[test]
605    fn test_system_use_endpoint_health_status() {
606        use environment_names::runtime::system;
607        temp_env::with_vars(
608            vec![(
609                system::DYN_SYSTEM_USE_ENDPOINT_HEALTH_STATUS,
610                Some("[\"ready\"]"),
611            )],
612            || {
613                let config = RuntimeConfig::from_settings().unwrap();
614                assert!(config.use_endpoint_health_status == vec!["ready"]);
615            },
616        );
617    }
618
619    #[test]
620    fn test_system_health_endpoint_path_default() {
621        use environment_names::runtime::system;
622        temp_env::with_vars(vec![(system::DYN_SYSTEM_HEALTH_PATH, None::<&str>)], || {
623            let config = RuntimeConfig::from_settings().unwrap();
624            assert_eq!(
625                config.system_health_path,
626                DEFAULT_SYSTEM_HEALTH_PATH.to_string()
627            );
628        });
629
630        temp_env::with_vars(vec![(system::DYN_SYSTEM_LIVE_PATH, None::<&str>)], || {
631            let config = RuntimeConfig::from_settings().unwrap();
632            assert_eq!(
633                config.system_live_path,
634                DEFAULT_SYSTEM_LIVE_PATH.to_string()
635            );
636        });
637    }
638
639    #[test]
640    fn test_system_health_endpoint_path_custom() {
641        use environment_names::runtime::system;
642        temp_env::with_vars(
643            vec![(system::DYN_SYSTEM_HEALTH_PATH, Some("/custom/health"))],
644            || {
645                let config = RuntimeConfig::from_settings().unwrap();
646                assert_eq!(config.system_health_path, "/custom/health");
647            },
648        );
649
650        temp_env::with_vars(
651            vec![(system::DYN_SYSTEM_LIVE_PATH, Some("/custom/live"))],
652            || {
653                let config = RuntimeConfig::from_settings().unwrap();
654                assert_eq!(config.system_live_path, "/custom/live");
655            },
656        );
657    }
658
659    #[test]
660    fn test_is_truthy_and_falsey() {
661        // Test truthy values
662        assert!(is_truthy("1"));
663        assert!(is_truthy("true"));
664        assert!(is_truthy("TRUE"));
665        assert!(is_truthy("on"));
666        assert!(is_truthy("yes"));
667
668        // Test falsey values
669        assert!(is_falsey("0"));
670        assert!(is_falsey("false"));
671        assert!(is_falsey("FALSE"));
672        assert!(is_falsey("off"));
673        assert!(is_falsey("no"));
674
675        // Test opposite behavior
676        assert!(!is_truthy("0"));
677        assert!(!is_falsey("1"));
678
679        // Test env functions
680        temp_env::with_vars(vec![("TEST_TRUTHY", Some("true"))], || {
681            assert!(env_is_truthy("TEST_TRUTHY"));
682            assert!(!env_is_falsey("TEST_TRUTHY"));
683        });
684
685        temp_env::with_vars(vec![("TEST_FALSEY", Some("false"))], || {
686            assert!(!env_is_truthy("TEST_FALSEY"));
687            assert!(env_is_falsey("TEST_FALSEY"));
688        });
689
690        // Test missing env vars
691        temp_env::with_vars(vec![("TEST_MISSING", None::<&str>)], || {
692            assert!(!env_is_truthy("TEST_MISSING"));
693            assert!(!env_is_falsey("TEST_MISSING"));
694        });
695    }
696}