@@ -204,149 +204,143 @@ const BYTES_PER_MIB: u64 = 1024 * 1024;
204204const MIN_MAX_FILE_SIZE : u64 = 16 * BYTES_PER_MIB ;
205205
206206trait EnvSource {
207- fn get ( & self , name : & str ) -> Option < String > ;
207+ fn get ( & self , name : & str ) -> Result < String , std :: env :: VarError > ;
208208}
209209
210210struct ProcessEnv ;
211211
212212impl EnvSource for ProcessEnv {
213- fn get ( & self , name : & str ) -> Option < String > {
214- std:: env:: var_os ( name) . map ( |value| value . to_string_lossy ( ) . into_owned ( ) )
213+ fn get ( & self , name : & str ) -> Result < String , std :: env :: VarError > {
214+ std:: env:: var ( name)
215215 }
216216}
217217
218+ impl EnvSourceParser for ProcessEnv { }
219+
218220#[ derive( Debug ) ]
219221struct ParsedEnvConfig {
220222 enabled : bool ,
221223 trace_dir : PathBuf ,
222224 rotation_period : Duration ,
223225 max_total_size : u64 ,
224226 max_file_size : u64 ,
225- warnings : Vec < String > ,
226227}
227228
228- fn parse_env_config ( env : & impl EnvSource ) -> ParsedEnvConfig {
229- let mut warnings = Vec :: new ( ) ;
230-
231- let enabled = parse_env_bool ( env, ENV_DIAL9_ENABLED , DEFAULT_ENABLED , & mut warnings) ;
232- let trace_dir = PathBuf :: from ( parse_env_string (
233- env,
234- ENV_DIAL9_TRACE_DIR ,
235- DEFAULT_TRACE_DIR ,
236- & mut warnings,
237- ) ) ;
238- let rotation_secs = parse_env_positive_u64 (
239- env,
240- ENV_DIAL9_ROTATION_SECS ,
241- DEFAULT_ROTATION_SECS ,
242- & mut warnings,
243- ) ;
244- let max_disk_usage_mb = parse_env_positive_u64 (
245- env,
246- ENV_DIAL9_MAX_DISK_USAGE_MB ,
247- DEFAULT_MAX_DISK_USAGE_MB ,
248- & mut warnings,
249- ) ;
250-
229+ fn parse_env_config ( env : & impl EnvSourceParser ) -> ParsedEnvConfig {
230+ let enabled = env. get_bool ( ENV_DIAL9_ENABLED , DEFAULT_ENABLED ) ;
231+ let trace_dir = PathBuf :: from ( env. get_string ( ENV_DIAL9_TRACE_DIR , DEFAULT_TRACE_DIR ) ) ;
232+ let rotation_secs = env. get_positive_u64 ( ENV_DIAL9_ROTATION_SECS , DEFAULT_ROTATION_SECS ) ;
233+ let max_disk_usage_mb =
234+ env. get_positive_u64 ( ENV_DIAL9_MAX_DISK_USAGE_MB , DEFAULT_MAX_DISK_USAGE_MB ) ;
251235 let max_total_size = max_disk_usage_mb. saturating_mul ( BYTES_PER_MIB ) ;
252- let max_file_size = ( max_total_size / 4 ) . max ( MIN_MAX_FILE_SIZE ) ;
253236
254237 ParsedEnvConfig {
255238 enabled,
256239 trace_dir,
257240 rotation_period : Duration :: from_secs ( rotation_secs) ,
258241 max_total_size,
259- max_file_size,
260- warnings,
242+ max_file_size : derive_max_file_size ( max_total_size) ,
261243 }
262244}
263245
264- fn parse_env_bool (
265- env : & impl EnvSource ,
266- name : & ' static str ,
267- default : bool ,
268- warnings : & mut Vec < String > ,
269- ) -> bool {
270- let Some ( value) = env. get ( name) else {
271- return default;
272- } ;
273- let value = value. trim ( ) ;
274- if value. is_empty ( ) {
275- warnings. push ( format ! (
276- "dial9: {name} is blank; expected an explicit boolean value; using default {default}"
277- ) ) ;
278- return default;
246+ trait EnvSourceParser : EnvSource {
247+ fn get_bool ( & self , name : & ' static str , default : bool ) -> bool {
248+ let value = match self . get ( name) {
249+ Ok ( value) => value,
250+ Err ( std:: env:: VarError :: NotPresent ) => return default,
251+ Err ( std:: env:: VarError :: NotUnicode ( _) ) => {
252+ self . warn_not_unicode ( name, default) ;
253+ return default;
254+ }
255+ } ;
256+ let value = value. trim ( ) ;
257+ if value. is_empty ( ) {
258+ self . warn ( format_args ! (
259+ "dial9: {name} is blank; expected an explicit boolean value; using default {default}"
260+ ) ) ;
261+ return default;
262+ }
263+
264+ match value. to_ascii_lowercase ( ) . as_str ( ) {
265+ "t" | "true" | "1" | "y" | "yes" | "on" => true ,
266+ "f" | "false" | "0" | "n" | "no" | "off" => false ,
267+ _ => {
268+ self . warn ( format_args ! (
269+ "dial9: {name}={value:?} is invalid; valid values are t,true,1,y,yes,on,f,false,0,n,no,off; using default {default}"
270+ ) ) ;
271+ default
272+ }
273+ }
279274 }
280275
281- match value. to_ascii_lowercase ( ) . as_str ( ) {
282- "t" | "true" | "1" | "y" | "yes" | "on" => true ,
283- "f" | "false" | "0" | "n" | "no" | "off" => false ,
284- _ => {
285- warnings. push ( format ! (
286- "dial9: {name}={value:?} is invalid; valid values are t,true,1,y,yes,on,f,false,0,n,no,off; using default {default}"
276+ fn get_positive_u64 ( & self , name : & ' static str , default : u64 ) -> u64 {
277+ let value = match self . get ( name) {
278+ Ok ( value) => value,
279+ Err ( std:: env:: VarError :: NotPresent ) => return default,
280+ Err ( std:: env:: VarError :: NotUnicode ( _) ) => {
281+ self . warn_not_unicode ( name, default) ;
282+ return default;
283+ }
284+ } ;
285+ let value = value. trim ( ) ;
286+ if value. is_empty ( ) {
287+ self . warn ( format_args ! (
288+ "dial9: {name} is blank; expected a positive integer; using default {default}"
287289 ) ) ;
288- default
290+ return default;
289291 }
290- }
291- }
292292
293- fn parse_env_positive_u64 (
294- env : & impl EnvSource ,
295- name : & ' static str ,
296- default : u64 ,
297- warnings : & mut Vec < String > ,
298- ) -> u64 {
299- let Some ( value) = env. get ( name) else {
300- return default;
301- } ;
302- let value = value. trim ( ) ;
303- if value. is_empty ( ) {
304- warnings. push ( format ! (
305- "dial9: {name} is blank; expected a positive integer; using default {default}"
306- ) ) ;
307- return default;
293+ match value. parse :: < u64 > ( ) {
294+ Ok ( n) if n > 0 => n,
295+ _ => {
296+ self . warn ( format_args ! (
297+ "dial9: {name}={value:?} is invalid; expected a positive integer; using default {default}"
298+ ) ) ;
299+ default
300+ }
301+ }
308302 }
309303
310- match value. parse :: < u64 > ( ) {
311- Ok ( n) if n > 0 => n,
312- _ => {
313- warnings. push ( format ! (
314- "dial9: {name}={value:?} is invalid; expected a positive integer; using default {default}"
304+ fn get_string ( & self , name : & ' static str , default : & ' static str ) -> String {
305+ let value = match self . get ( name) {
306+ Ok ( value) => value,
307+ Err ( std:: env:: VarError :: NotPresent ) => return default. to_string ( ) ,
308+ Err ( std:: env:: VarError :: NotUnicode ( _) ) => {
309+ self . warn_not_unicode ( name, default) ;
310+ return default. to_string ( ) ;
311+ }
312+ } ;
313+ let value = value. trim ( ) ;
314+ if value. is_empty ( ) {
315+ self . warn ( format_args ! (
316+ "dial9: {name} is blank; expected a non-empty value; using default {default:?}"
315317 ) ) ;
316- default
318+ return default. to_string ( ) ;
317319 }
320+ value. to_string ( )
318321 }
319- }
320322
321- fn parse_env_string (
322- env : & impl EnvSource ,
323- name : & ' static str ,
324- default : & ' static str ,
325- warnings : & mut Vec < String > ,
326- ) -> String {
327- let Some ( value) = env. get ( name) else {
328- return default. to_string ( ) ;
329- } ;
330- let value = value. trim ( ) ;
331- if value. is_empty ( ) {
332- warnings. push ( format ! (
333- "dial9: {name} is blank; expected a non-empty value; using default {default:?}"
323+ fn warn_not_unicode ( & self , name : & ' static str , default : impl fmt:: Display ) {
324+ self . warn ( format_args ! (
325+ "dial9: {name} is not valid Unicode; using default {default}"
334326 ) ) ;
335- return default. to_string ( ) ;
336327 }
337- value. to_string ( )
338- }
339328
340- fn emit_env_warnings ( warnings : & [ String ] ) {
341- for warning in warnings {
329+ fn warn ( & self , message : fmt:: Arguments < ' _ > ) {
342330 if tracing:: dispatcher:: has_been_set ( ) {
343- tracing:: warn!( target: "dial9_telemetry" , "{warning }" ) ;
331+ tracing:: warn!( target: "dial9_telemetry" , "{message }" ) ;
344332 } else {
345- eprintln ! ( "{warning }" ) ;
333+ eprintln ! ( "{message }" ) ;
346334 }
347335 }
348336}
349337
338+ fn derive_max_file_size ( max_total_size : u64 ) -> u64 {
339+ // Keep size-based rotation as a safety valve: roughly four large segments
340+ // fit in the disk budget, but avoid tiny files on very small budgets.
341+ ( max_total_size / 4 ) . max ( MIN_MAX_FILE_SIZE )
342+ }
343+
350344fn default_tokio_builder ( ) -> tokio:: runtime:: Builder {
351345 let mut b = tokio:: runtime:: Builder :: new_multi_thread ( ) ;
352346 b. enable_all ( ) ;
@@ -356,7 +350,7 @@ fn default_tokio_builder() -> tokio::runtime::Builder {
356350impl Dial9Config {
357351 /// Build a production-oriented config from standard `DIAL9_*` environment variables.
358352 ///
359- /// Supported variables in this first iteration :
353+ /// Supported local trace writer variables :
360354 ///
361355 /// | Variable | Default | Meaning |
362356 /// | --- | --- | --- |
@@ -373,9 +367,8 @@ impl Dial9Config {
373367 Self :: from_env_source ( & ProcessEnv )
374368 }
375369
376- fn from_env_source ( env : & impl EnvSource ) -> Self {
370+ fn from_env_source ( env : & impl EnvSourceParser ) -> Self {
377371 let parsed = parse_env_config ( env) ;
378- emit_env_warnings ( & parsed. warnings ) ;
379372
380373 Self :: builder ( )
381374 . enabled ( parsed. enabled )
@@ -603,6 +596,7 @@ impl<S: dial9_config_builder::IsComplete> Dial9ConfigBuilder<S> {
603596#[ cfg( test) ]
604597mod tests {
605598 use std:: collections:: BTreeMap ;
599+ use std:: ffi:: OsString ;
606600 use std:: sync:: Arc ;
607601 use std:: sync:: atomic:: { AtomicUsize , Ordering } ;
608602
@@ -626,22 +620,41 @@ mod tests {
626620
627621 #[ derive( Default ) ]
628622 struct FakeEnv {
629- vars : BTreeMap < String , String > ,
623+ vars : BTreeMap < String , FakeEnvValue > ,
624+ }
625+
626+ enum FakeEnvValue {
627+ Unicode ( String ) ,
628+ NonUnicode ,
630629 }
631630
632631 impl FakeEnv {
633632 fn with ( mut self , name : impl Into < String > , value : impl Into < String > ) -> Self {
634- self . vars . insert ( name. into ( ) , value. into ( ) ) ;
633+ self . vars
634+ . insert ( name. into ( ) , FakeEnvValue :: Unicode ( value. into ( ) ) ) ;
635+ self
636+ }
637+
638+ fn with_non_unicode ( mut self , name : impl Into < String > ) -> Self {
639+ self . vars . insert ( name. into ( ) , FakeEnvValue :: NonUnicode ) ;
635640 self
636641 }
637642 }
638643
639644 impl EnvSource for FakeEnv {
640- fn get ( & self , name : & str ) -> Option < String > {
641- self . vars . get ( name) . cloned ( )
645+ fn get ( & self , name : & str ) -> Result < String , std:: env:: VarError > {
646+ match self . vars . get ( name) {
647+ Some ( FakeEnvValue :: Unicode ( value) ) => Ok ( value. clone ( ) ) ,
648+ Some ( FakeEnvValue :: NonUnicode ) => Err ( std:: env:: VarError :: NotUnicode (
649+ OsString :: from ( "not unicode" ) ,
650+ ) ) ,
651+ None => Err ( std:: env:: VarError :: NotPresent ) ,
652+ }
642653 }
643654 }
644655
656+ impl EnvSourceParser for FakeEnv { }
657+
645658 #[ test]
646659 fn env_defaults_to_disabled_local_traces ( ) {
647660 let parsed = parse_env_config ( & FakeEnv :: default ( ) ) ;
@@ -651,7 +664,6 @@ mod tests {
651664 assert_eq ! ( parsed. rotation_period, Duration :: from_secs( 60 ) ) ;
652665 assert_eq ! ( parsed. max_total_size, 1024 * 1024 * 1024 ) ;
653666 assert_eq ! ( parsed. max_file_size, 256 * 1024 * 1024 ) ;
654- assert ! ( parsed. warnings. is_empty( ) ) ;
655667 }
656668
657669 #[ test]
@@ -669,7 +681,6 @@ mod tests {
669681 assert_eq ! ( parsed. rotation_period, Duration :: from_secs( 15 ) ) ;
670682 assert_eq ! ( parsed. max_total_size, 2048 * 1024 * 1024 ) ;
671683 assert_eq ! ( parsed. max_file_size, 512 * 1024 * 1024 ) ;
672- assert ! ( parsed. warnings. is_empty( ) ) ;
673684 }
674685
675686 #[ test]
@@ -687,26 +698,18 @@ mod tests {
687698 assert_eq ! ( parsed. rotation_period, Duration :: from_secs( 60 ) ) ;
688699 assert_eq ! ( parsed. max_total_size, 1024 * 1024 * 1024 ) ;
689700 assert_eq ! ( parsed. max_file_size, 256 * 1024 * 1024 ) ;
690- assert_eq ! ( parsed. warnings. len( ) , 4 ) ;
691- assert ! ( parsed. warnings. iter( ) . any( |w| w. contains( "DIAL9_ENABLED" ) ) ) ;
692- assert ! (
693- parsed
694- . warnings
695- . iter( )
696- . any( |w| w. contains( "DIAL9_TRACE_DIR" ) )
697- ) ;
698- assert ! (
699- parsed
700- . warnings
701- . iter( )
702- . any( |w| w. contains( "DIAL9_ROTATION_SECS" ) )
703- ) ;
704- assert ! (
705- parsed
706- . warnings
707- . iter( )
708- . any( |w| w. contains( "DIAL9_MAX_DISK_USAGE_MB" ) )
701+ }
702+
703+ #[ test]
704+ fn env_treats_non_unicode_values_as_invalid ( ) {
705+ let parsed = parse_env_config (
706+ & FakeEnv :: default ( )
707+ . with_non_unicode ( "DIAL9_TRACE_DIR" )
708+ . with_non_unicode ( "DIAL9_ROTATION_SECS" ) ,
709709 ) ;
710+
711+ assert_eq ! ( parsed. trace_dir, PathBuf :: from( "/tmp/dial9-traces" ) ) ;
712+ assert_eq ! ( parsed. rotation_period, Duration :: from_secs( 60 ) ) ;
710713 }
711714
712715 #[ test]
0 commit comments