provide_telemetry/config/
probe.rs1use std::collections::BTreeMap;
19use std::collections::HashMap;
20
21use serde::Serialize;
22use serde_json::Value;
23
24use super::TelemetryConfig;
25
26const PROBE_VALUES: &[&str] = &[
30 "DEBUG",
31 "json",
32 "red",
33 "3",
34 "1327",
35 "0.4271",
36 "probe-sentinel-value",
37 "false",
38 "true",
39 "http://probe.invalid:4318",
40 "probe-module=DEBUG",
41 "probe-key=probe-value",
42];
43
44#[derive(Clone, Debug, PartialEq, Eq, Serialize)]
46pub struct ProbedConfigEntry {
47 #[serde(rename = "type")]
48 pub type_name: String,
49 pub default: String,
50 pub applicable: bool,
51}
52
53impl ProbedConfigEntry {
54 fn unsupported() -> Self {
55 Self {
56 type_name: String::new(),
57 default: String::new(),
58 applicable: false,
59 }
60 }
61}
62
63fn flatten(value: &Value, prefix: &str, out: &mut BTreeMap<String, (String, String)>) {
66 match value {
67 Value::Object(map) => {
68 for (key, nested) in map {
69 flatten(nested, &format!("{prefix}{key}."), out);
70 }
71 }
72 Value::Array(items) => {
73 let joined = items
74 .iter()
75 .map(render_scalar)
76 .collect::<Vec<_>>()
77 .join(",");
78 insert_flat(out, prefix, joined, "str");
79 }
80 Value::Bool(flag) => insert_flat(out, prefix, flag.to_string(), "bool"),
81 Value::Number(number) => {
82 let type_name = if number.is_i64() || number.is_u64() {
83 "int"
84 } else {
85 "float"
86 };
87 insert_flat(out, prefix, render_scalar(value), type_name);
88 }
89 Value::Null => insert_flat(out, prefix, String::new(), "str"),
90 Value::String(text) => insert_flat(out, prefix, text.clone(), "str"),
91 }
92}
93
94fn insert_flat(
95 out: &mut BTreeMap<String, (String, String)>,
96 prefix: &str,
97 value: String,
98 type_name: &str,
99) {
100 out.insert(
101 prefix.trim_end_matches('.').to_string(),
102 (value, type_name.to_string()),
103 );
104}
105
106fn render_scalar(value: &Value) -> String {
109 match value {
110 Value::String(text) => text.clone(),
111 other => other.to_string(),
112 }
113}
114
115fn build(env: &HashMap<String, String>) -> Option<BTreeMap<String, (String, String)>> {
116 let config = TelemetryConfig::from_map(env).ok()?;
117 let serialized = serde_json::to_value(&config).ok()?;
118 let mut flat = BTreeMap::new();
119 flatten(&serialized, "", &mut flat);
120 Some(flat)
121}
122
123fn default_in_variable_units(baseline: &str, probe_value: &str, observed: &str) -> String {
129 let (Ok(base), Ok(probed), Ok(obs)) = (
130 baseline.parse::<f64>(),
131 probe_value.parse::<f64>(),
132 observed.parse::<f64>(),
133 ) else {
134 return baseline.to_string();
135 };
136 let scale = obs / probed;
140 if scale == 1.0 || scale <= 0.0 || scale.fract() != 0.0 {
141 return baseline.to_string();
142 }
143 let scaled = base / scale;
144 if scaled.fract() == 0.0 {
145 format!("{}", scaled as i64)
146 } else {
147 format!("{scaled}")
148 }
149}
150
151fn probe_one(
159 baseline: &BTreeMap<String, (String, String)>,
160 env_var: &str,
161 probe_values: &[&str],
162) -> Option<ProbedConfigEntry> {
163 let mut rejected = false;
164 for probe_value in probe_values {
165 let env: HashMap<String, String> =
166 HashMap::from([(env_var.to_string(), (*probe_value).to_string())]);
167 let Some(observed) = build(&env) else {
168 rejected = true;
170 continue;
171 };
172
173 if let Some(key) = baseline
174 .iter()
175 .find(|(key, (value, _))| observed.get(*key).is_some_and(|(other, _)| other != value))
176 .map(|(key, _)| key)
177 {
178 let (base_value, base_type) = &baseline[key];
179 let (observed_value, _) = &observed[key];
180 return Some(ProbedConfigEntry {
181 type_name: base_type.clone(),
182 default: default_in_variable_units(base_value, probe_value, observed_value),
183 applicable: true,
184 });
185 }
186
187 if observed.keys().any(|key| !baseline.contains_key(key)) {
191 return Some(ProbedConfigEntry {
192 type_name: "str".to_string(),
193 default: String::new(),
194 applicable: true,
195 });
196 }
197 }
198 rejected.then(|| ProbedConfigEntry {
199 applicable: true,
200 ..ProbedConfigEntry::unsupported()
201 })
202}
203
204pub fn config_defaults_probe(env_vars: &[String]) -> BTreeMap<String, ProbedConfigEntry> {
211 let baseline = build(&HashMap::new()).expect("an empty environment must yield a valid config");
212 env_vars
213 .iter()
214 .map(|env_var| {
215 let entry = probe_one(&baseline, env_var, PROBE_VALUES)
216 .unwrap_or_else(ProbedConfigEntry::unsupported);
217 (env_var.clone(), entry)
218 })
219 .collect()
220}
221
222#[cfg(test)]
223#[path = "probe_tests.rs"]
224mod tests;