use std::collections::BTreeMap;
use std::collections::HashMap;
use serde::Serialize;
use serde_json::Value;
use super::TelemetryConfig;
const PROBE_VALUES: &[&str] = &[
"DEBUG",
"json",
"red",
"3",
"1327",
"0.4271",
"probe-sentinel-value",
"false",
"true",
"http://probe.invalid:4318",
"probe-module=DEBUG",
"probe-key=probe-value",
];
#[derive(Clone, Debug, PartialEq, Eq, Serialize)]
pub struct ProbedConfigEntry {
#[serde(rename = "type")]
pub type_name: String,
pub default: String,
pub applicable: bool,
}
impl ProbedConfigEntry {
fn unsupported() -> Self {
Self {
type_name: String::new(),
default: String::new(),
applicable: false,
}
}
}
fn flatten(value: &Value, prefix: &str, out: &mut BTreeMap<String, (String, String)>) {
match value {
Value::Object(map) => {
for (key, nested) in map {
flatten(nested, &format!("{prefix}{key}."), out);
}
}
Value::Array(items) => {
let joined = items
.iter()
.map(render_scalar)
.collect::<Vec<_>>()
.join(",");
insert_flat(out, prefix, joined, "str");
}
Value::Bool(flag) => insert_flat(out, prefix, flag.to_string(), "bool"),
Value::Number(number) => {
let type_name = if number.is_i64() || number.is_u64() {
"int"
} else {
"float"
};
insert_flat(out, prefix, render_scalar(value), type_name);
}
Value::Null => insert_flat(out, prefix, String::new(), "str"),
Value::String(text) => insert_flat(out, prefix, text.clone(), "str"),
}
}
fn insert_flat(
out: &mut BTreeMap<String, (String, String)>,
prefix: &str,
value: String,
type_name: &str,
) {
out.insert(
prefix.trim_end_matches('.').to_string(),
(value, type_name.to_string()),
);
}
fn render_scalar(value: &Value) -> String {
match value {
Value::String(text) => text.clone(),
other => other.to_string(),
}
}
fn build(env: &HashMap<String, String>) -> Option<BTreeMap<String, (String, String)>> {
let config = TelemetryConfig::from_map(env).ok()?;
let serialized = serde_json::to_value(&config).ok()?;
let mut flat = BTreeMap::new();
flatten(&serialized, "", &mut flat);
Some(flat)
}
fn default_in_variable_units(baseline: &str, probe_value: &str, observed: &str) -> String {
let (Ok(base), Ok(probed), Ok(obs)) = (
baseline.parse::<f64>(),
probe_value.parse::<f64>(),
observed.parse::<f64>(),
) else {
return baseline.to_string();
};
let scale = obs / probed;
if scale == 1.0 || scale <= 0.0 || scale.fract() != 0.0 {
return baseline.to_string();
}
let scaled = base / scale;
if scaled.fract() == 0.0 {
format!("{}", scaled as i64)
} else {
format!("{scaled}")
}
}
fn probe_one(
baseline: &BTreeMap<String, (String, String)>,
env_var: &str,
probe_values: &[&str],
) -> Option<ProbedConfigEntry> {
let mut rejected = false;
for probe_value in probe_values {
let env: HashMap<String, String> =
HashMap::from([(env_var.to_string(), (*probe_value).to_string())]);
let Some(observed) = build(&env) else {
rejected = true;
continue;
};
if let Some(key) = baseline
.iter()
.find(|(key, (value, _))| observed.get(*key).is_some_and(|(other, _)| other != value))
.map(|(key, _)| key)
{
let (base_value, base_type) = &baseline[key];
let (observed_value, _) = &observed[key];
return Some(ProbedConfigEntry {
type_name: base_type.clone(),
default: default_in_variable_units(base_value, probe_value, observed_value),
applicable: true,
});
}
if observed.keys().any(|key| !baseline.contains_key(key)) {
return Some(ProbedConfigEntry {
type_name: "str".to_string(),
default: String::new(),
applicable: true,
});
}
}
rejected.then(|| ProbedConfigEntry {
applicable: true,
..ProbedConfigEntry::unsupported()
})
}
pub fn config_defaults_probe(env_vars: &[String]) -> BTreeMap<String, ProbedConfigEntry> {
let baseline = build(&HashMap::new()).expect("an empty environment must yield a valid config");
env_vars
.iter()
.map(|env_var| {
let entry = probe_one(&baseline, env_var, PROBE_VALUES)
.unwrap_or_else(ProbedConfigEntry::unsupported);
(env_var.clone(), entry)
})
.collect()
}
#[cfg(test)]
#[path = "probe_tests.rs"]
mod tests;