Skip to content

Commit 1bfca65

Browse files
committed
env var parser trait
1 parent e5589f6 commit 1bfca65

2 files changed

Lines changed: 128 additions & 125 deletions

File tree

dial9-tokio-telemetry/README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -76,7 +76,7 @@ async fn main() {
7676
}
7777
```
7878

79-
`from_env()` currently supports the local trace writer knobs:
79+
`from_env()` supports these local trace writer knobs:
8080

8181
| Name | Default | Meaning |
8282
| --- | --- | --- |

dial9-tokio-telemetry/src/config.rs

Lines changed: 127 additions & 124 deletions
Original file line numberDiff line numberDiff line change
@@ -204,149 +204,143 @@ const BYTES_PER_MIB: u64 = 1024 * 1024;
204204
const MIN_MAX_FILE_SIZE: u64 = 16 * BYTES_PER_MIB;
205205

206206
trait EnvSource {
207-
fn get(&self, name: &str) -> Option<String>;
207+
fn get(&self, name: &str) -> Result<String, std::env::VarError>;
208208
}
209209

210210
struct ProcessEnv;
211211

212212
impl 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)]
219221
struct 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+
350344
fn 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 {
356350
impl 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)]
604597
mod 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

Comments
 (0)