Skip to main content

premortem/
trace.rs

1//! Value tracing and origin tracking for debugging configuration issues.
2//!
3//! This module provides types and methods for tracking where configuration values
4//! came from and their override history across multiple sources.
5//!
6//! # Overview
7//!
8//! When debugging configuration issues, especially in production, it's crucial
9//! to know where each value came from. With multiple sources (defaults, file,
10//! environment), a value might be set in one place and overridden in another.
11//! Value tracing shows the complete history of each configuration path.
12//!
13//! # Usage
14//!
15//! ```ignore
16//! use premortem::{Config, TracedConfig};
17//!
18//! let traced = Config::<AppConfig>::builder()
19//!     .source(Defaults::from(AppConfig::default()))
20//!     .source(Toml::file("config.toml"))
21//!     .source(Env::prefix("APP_"))
22//!     .build_traced()?;
23//!
24//! // Query specific path
25//! if let Some(trace) = traced.trace("database.host") {
26//!     println!("database.host = {:?}", trace.final_value.value);
27//!     println!("  from: {}", trace.final_value.source);
28//! }
29//!
30//! // Check for overrides
31//! if traced.was_overridden("database.host") {
32//!     println!("Warning: database.host was overridden");
33//! }
34//!
35//! // Generate full report
36//! println!("{}", traced.trace_report());
37//! ```
38
39use std::collections::BTreeMap;
40use std::fmt;
41
42use crate::config::Config;
43use crate::error::SourceLocation;
44use crate::value::Value;
45
46/// A value with its source information.
47#[derive(Debug, Clone)]
48pub struct TracedValue {
49    /// The value at this source
50    pub value: Value,
51    /// Where this value came from
52    pub source: SourceLocation,
53    /// Whether this value was used (not overridden)
54    pub is_final: bool,
55}
56
57impl TracedValue {
58    /// Create a new traced value.
59    pub fn new(value: Value, source: SourceLocation, is_final: bool) -> Self {
60        Self {
61            value,
62            source,
63            is_final,
64        }
65    }
66}
67
68/// Trace of a single configuration value.
69#[derive(Debug, Clone)]
70pub struct ValueTrace {
71    /// The final value (from highest priority source)
72    pub final_value: TracedValue,
73    /// All values from all sources, in priority order (lowest first)
74    pub history: Vec<TracedValue>,
75}
76
77impl ValueTrace {
78    /// Create a new value trace from a history of traced values.
79    ///
80    /// The last value in the history is considered the final value.
81    /// The `is_final` flag is automatically set on the last value.
82    pub fn new(mut history: Vec<TracedValue>) -> Option<Self> {
83        if history.is_empty() {
84            return None;
85        }
86
87        // Mark the last value as final
88        if let Some(last) = history.last_mut() {
89            last.is_final = true;
90        }
91
92        let final_value = history.last().cloned()?;
93
94        Some(Self {
95            final_value,
96            history,
97        })
98    }
99
100    /// Check if this value was overridden (has more than one source).
101    pub fn was_overridden(&self) -> bool {
102        self.history.len() > 1
103    }
104
105    /// Get the number of sources that provided this value.
106    pub fn source_count(&self) -> usize {
107        self.history.len()
108    }
109}
110
111impl fmt::Display for ValueTrace {
112    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
113        writeln!(
114            f,
115            "Final: {:?} (from {})",
116            self.final_value.value, self.final_value.source
117        )?;
118
119        if self.history.len() > 1 {
120            writeln!(f, "History:")?;
121            for val in &self.history {
122                let marker = if val.is_final { "→" } else { " " };
123                writeln!(f, "  {} [{}] {:?}", marker, val.source, val.value)?;
124            }
125        }
126
127        Ok(())
128    }
129}
130
131/// Configuration with tracing information.
132///
133/// This type wraps a `Config<T>` with additional trace data that shows
134/// where each configuration value came from and its override history.
135#[derive(Debug)]
136pub struct TracedConfig<T> {
137    config: Config<T>,
138    traces: BTreeMap<String, ValueTrace>,
139}
140
141impl<T> TracedConfig<T> {
142    /// Create a new traced config from a config and traces.
143    pub fn new(config: Config<T>, traces: BTreeMap<String, ValueTrace>) -> Self {
144        Self { config, traces }
145    }
146
147    /// Get reference to the configuration.
148    pub fn value(&self) -> &T {
149        self.config.get()
150    }
151
152    /// Get reference to the inner Config.
153    pub fn config(&self) -> &Config<T> {
154        &self.config
155    }
156
157    /// Consume and return the configuration value.
158    pub fn into_inner(self) -> T {
159        self.config.into_inner()
160    }
161
162    /// Consume and return the inner Config.
163    pub fn into_config(self) -> Config<T> {
164        self.config
165    }
166
167    /// Get the trace for a specific path.
168    pub fn trace(&self, path: &str) -> Option<&ValueTrace> {
169        self.traces.get(path)
170    }
171
172    /// Check if a path was overridden by a higher-priority source.
173    pub fn was_overridden(&self, path: &str) -> bool {
174        self.traces
175            .get(path)
176            .map(|t| t.history.len() > 1)
177            .unwrap_or(false)
178    }
179
180    /// Get all traces.
181    pub fn traces(&self) -> impl Iterator<Item = (&str, &ValueTrace)> {
182        self.traces.iter().map(|(k, v)| (k.as_str(), v))
183    }
184
185    /// Get paths that were overridden.
186    pub fn overridden_paths(&self) -> impl Iterator<Item = &str> {
187        self.traces
188            .iter()
189            .filter(|(_, t)| t.history.len() > 1)
190            .map(|(k, _)| k.as_str())
191    }
192
193    /// Get all traced paths.
194    pub fn paths(&self) -> impl Iterator<Item = &str> {
195        self.traces.keys().map(|k| k.as_str())
196    }
197
198    /// Get the number of traced paths.
199    pub fn trace_count(&self) -> usize {
200        self.traces.len()
201    }
202
203    /// Generate a human-readable trace report.
204    pub fn trace_report(&self) -> String {
205        let mut report = String::new();
206
207        for (path, trace) in &self.traces {
208            report.push_str(&format!("{} = {:?}\n", path, trace.final_value.value));
209
210            for val in &trace.history {
211                let marker = if val.is_final { "✓" } else { "○" };
212                let override_note = if !val.is_final { " <- overridden" } else { "" };
213                report.push_str(&format!(
214                    "  {} [{}] {:?}{}\n",
215                    marker, val.source, val.value, override_note
216                ));
217            }
218            report.push('\n');
219        }
220
221        report
222    }
223}
224
225impl<T> std::ops::Deref for TracedConfig<T> {
226    type Target = T;
227
228    fn deref(&self) -> &Self::Target {
229        self.config.get()
230    }
231}
232
233impl<T> AsRef<T> for TracedConfig<T> {
234    fn as_ref(&self) -> &T {
235        self.config.get()
236    }
237}
238
239/// Builder for collecting trace data during config building.
240///
241/// This is used internally by `ConfigBuilder::build_traced()`.
242#[derive(Debug, Default)]
243pub struct TraceBuilder {
244    /// Values collected from all sources, keyed by path.
245    /// Each path maps to a list of (value, source) pairs in source order.
246    values: BTreeMap<String, Vec<TracedValue>>,
247}
248
249impl TraceBuilder {
250    /// Create a new trace builder.
251    pub fn new() -> Self {
252        Self {
253            values: BTreeMap::new(),
254        }
255    }
256
257    /// Add a value from a source.
258    pub fn add_value(&mut self, path: String, value: Value, source: SourceLocation) {
259        self.values
260            .entry(path)
261            .or_default()
262            .push(TracedValue::new(value, source, false));
263    }
264
265    /// Build the final traces map.
266    pub fn build(self) -> BTreeMap<String, ValueTrace> {
267        self.values
268            .into_iter()
269            .filter_map(|(path, history)| ValueTrace::new(history).map(|trace| (path, trace)))
270            .collect()
271    }
272}
273
274#[cfg(test)]
275mod tests {
276    use super::*;
277    use crate::config::Config;
278
279    #[test]
280    fn test_traced_value_new() {
281        let tv = TracedValue::new(
282            Value::String("localhost".to_string()),
283            SourceLocation::new("config.toml"),
284            false,
285        );
286
287        assert_eq!(tv.value.as_str(), Some("localhost"));
288        assert_eq!(tv.source.source, "config.toml");
289        assert!(!tv.is_final);
290    }
291
292    #[test]
293    fn test_value_trace_new() {
294        let history = vec![
295            TracedValue::new(
296                Value::String("default".to_string()),
297                SourceLocation::new("defaults"),
298                false,
299            ),
300            TracedValue::new(
301                Value::String("override".to_string()),
302                SourceLocation::new("config.toml"),
303                false,
304            ),
305        ];
306
307        let trace = ValueTrace::new(history).unwrap();
308
309        assert_eq!(trace.final_value.value.as_str(), Some("override"));
310        assert!(trace.final_value.is_final);
311        assert!(trace.was_overridden());
312        assert_eq!(trace.source_count(), 2);
313    }
314
315    #[test]
316    fn test_value_trace_single_source() {
317        let history = vec![TracedValue::new(
318            Value::Integer(8080),
319            SourceLocation::new("config.toml"),
320            false,
321        )];
322
323        let trace = ValueTrace::new(history).unwrap();
324
325        assert!(!trace.was_overridden());
326        assert_eq!(trace.source_count(), 1);
327    }
328
329    #[test]
330    fn test_value_trace_empty_history() {
331        let trace = ValueTrace::new(vec![]);
332        assert!(trace.is_none());
333    }
334
335    #[test]
336    fn test_value_trace_display() {
337        let history = vec![
338            TracedValue::new(
339                Value::String("localhost".to_string()),
340                SourceLocation::new("defaults"),
341                false,
342            ),
343            TracedValue::new(
344                Value::String("prod-db".to_string()),
345                SourceLocation::new("env:DB_HOST"),
346                true,
347            ),
348        ];
349
350        let trace = ValueTrace::new(history).unwrap();
351        let display = format!("{}", trace);
352
353        assert!(display.contains("Final:"));
354        assert!(display.contains("prod-db"));
355        assert!(display.contains("History:"));
356    }
357
358    #[test]
359    fn test_traced_config_basic() {
360        #[allow(dead_code)]
361        #[derive(Debug)]
362        struct TestConfig {
363            host: String,
364            port: i64,
365        }
366
367        let config = Config::new(TestConfig {
368            host: "localhost".to_string(),
369            port: 8080,
370        });
371
372        let mut traces = BTreeMap::new();
373        traces.insert(
374            "host".to_string(),
375            ValueTrace::new(vec![TracedValue::new(
376                Value::String("localhost".to_string()),
377                SourceLocation::new("config.toml"),
378                false,
379            )])
380            .unwrap(),
381        );
382
383        let traced = TracedConfig::new(config, traces);
384
385        assert_eq!(traced.value().host, "localhost");
386        assert_eq!(traced.trace_count(), 1);
387        assert!(traced.trace("host").is_some());
388        assert!(traced.trace("nonexistent").is_none());
389    }
390
391    #[test]
392    fn test_traced_config_was_overridden() {
393        #[allow(dead_code)]
394        #[derive(Debug)]
395        struct TestConfig {
396            value: String,
397        }
398
399        let config = Config::new(TestConfig {
400            value: "final".to_string(),
401        });
402
403        let mut traces = BTreeMap::new();
404
405        // Single source - not overridden
406        traces.insert(
407            "single".to_string(),
408            ValueTrace::new(vec![TracedValue::new(
409                Value::String("only".to_string()),
410                SourceLocation::new("defaults"),
411                false,
412            )])
413            .unwrap(),
414        );
415
416        // Multiple sources - overridden
417        traces.insert(
418            "overridden".to_string(),
419            ValueTrace::new(vec![
420                TracedValue::new(
421                    Value::String("first".to_string()),
422                    SourceLocation::new("defaults"),
423                    false,
424                ),
425                TracedValue::new(
426                    Value::String("second".to_string()),
427                    SourceLocation::new("config.toml"),
428                    false,
429                ),
430            ])
431            .unwrap(),
432        );
433
434        let traced = TracedConfig::new(config, traces);
435
436        assert!(!traced.was_overridden("single"));
437        assert!(traced.was_overridden("overridden"));
438        assert!(!traced.was_overridden("nonexistent"));
439    }
440
441    #[test]
442    fn test_traced_config_overridden_paths() {
443        #[derive(Debug)]
444        struct TestConfig;
445
446        let config = Config::new(TestConfig);
447
448        let mut traces = BTreeMap::new();
449
450        traces.insert(
451            "a".to_string(),
452            ValueTrace::new(vec![TracedValue::new(
453                Value::Integer(1),
454                SourceLocation::new("defaults"),
455                false,
456            )])
457            .unwrap(),
458        );
459
460        traces.insert(
461            "b".to_string(),
462            ValueTrace::new(vec![
463                TracedValue::new(Value::Integer(1), SourceLocation::new("defaults"), false),
464                TracedValue::new(Value::Integer(2), SourceLocation::new("file"), false),
465            ])
466            .unwrap(),
467        );
468
469        traces.insert(
470            "c".to_string(),
471            ValueTrace::new(vec![
472                TracedValue::new(Value::Integer(1), SourceLocation::new("defaults"), false),
473                TracedValue::new(Value::Integer(2), SourceLocation::new("file"), false),
474                TracedValue::new(Value::Integer(3), SourceLocation::new("env"), false),
475            ])
476            .unwrap(),
477        );
478
479        let traced = TracedConfig::new(config, traces);
480
481        let overridden: Vec<&str> = traced.overridden_paths().collect();
482        assert_eq!(overridden.len(), 2);
483        assert!(overridden.contains(&"b"));
484        assert!(overridden.contains(&"c"));
485    }
486
487    #[test]
488    fn test_traced_config_trace_report() {
489        #[derive(Debug)]
490        struct TestConfig;
491
492        let config = Config::new(TestConfig);
493
494        let mut traces = BTreeMap::new();
495
496        traces.insert(
497            "database.host".to_string(),
498            ValueTrace::new(vec![
499                TracedValue::new(
500                    Value::String("localhost".to_string()),
501                    SourceLocation::new("defaults"),
502                    false,
503                ),
504                TracedValue::new(
505                    Value::String("prod-db".to_string()),
506                    SourceLocation::new("env:DB_HOST"),
507                    false,
508                ),
509            ])
510            .unwrap(),
511        );
512
513        traces.insert(
514            "database.port".to_string(),
515            ValueTrace::new(vec![TracedValue::new(
516                Value::Integer(5432),
517                SourceLocation::new("config.toml"),
518                false,
519            )])
520            .unwrap(),
521        );
522
523        let traced = TracedConfig::new(config, traces);
524        let report = traced.trace_report();
525
526        assert!(report.contains("database.host"));
527        assert!(report.contains("prod-db"));
528        assert!(report.contains("<- overridden"));
529        assert!(report.contains("database.port"));
530        assert!(report.contains("5432"));
531        assert!(report.contains("✓"));
532        assert!(report.contains("○"));
533    }
534
535    #[test]
536    fn test_trace_builder() {
537        let mut builder = TraceBuilder::new();
538
539        builder.add_value(
540            "host".to_string(),
541            Value::String("localhost".to_string()),
542            SourceLocation::new("defaults"),
543        );
544        builder.add_value(
545            "host".to_string(),
546            Value::String("prod".to_string()),
547            SourceLocation::new("env"),
548        );
549        builder.add_value(
550            "port".to_string(),
551            Value::Integer(8080),
552            SourceLocation::new("defaults"),
553        );
554
555        let traces = builder.build();
556
557        assert_eq!(traces.len(), 2);
558        assert!(traces.get("host").unwrap().was_overridden());
559        assert!(!traces.get("port").unwrap().was_overridden());
560    }
561
562    #[test]
563    fn test_traced_config_deref() {
564        #[derive(Debug)]
565        struct TestConfig {
566            value: i32,
567        }
568
569        let config = Config::new(TestConfig { value: 42 });
570        let traced = TracedConfig::new(config, BTreeMap::new());
571
572        // Test Deref
573        assert_eq!(traced.value, 42);
574
575        // Test AsRef
576        let r: &TestConfig = traced.as_ref();
577        assert_eq!(r.value, 42);
578    }
579
580    #[test]
581    fn test_traced_config_into_inner() {
582        #[derive(Debug, PartialEq)]
583        struct TestConfig {
584            value: i32,
585        }
586
587        let config = Config::new(TestConfig { value: 42 });
588        let traced = TracedConfig::new(config, BTreeMap::new());
589
590        let inner = traced.into_inner();
591        assert_eq!(inner, TestConfig { value: 42 });
592    }
593
594    #[test]
595    fn test_traced_config_into_config() {
596        #[derive(Debug)]
597        struct TestConfig {
598            value: i32,
599        }
600
601        let config = Config::new(TestConfig { value: 42 });
602        let traced = TracedConfig::new(config, BTreeMap::new());
603
604        let config = traced.into_config();
605        assert_eq!(config.get().value, 42);
606    }
607
608    #[test]
609    fn test_traced_config_paths() {
610        #[derive(Debug)]
611        struct TestConfig;
612
613        let config = Config::new(TestConfig);
614
615        let mut traces = BTreeMap::new();
616        traces.insert(
617            "a".to_string(),
618            ValueTrace::new(vec![TracedValue::new(
619                Value::Integer(1),
620                SourceLocation::new("test"),
621                false,
622            )])
623            .unwrap(),
624        );
625        traces.insert(
626            "b".to_string(),
627            ValueTrace::new(vec![TracedValue::new(
628                Value::Integer(2),
629                SourceLocation::new("test"),
630                false,
631            )])
632            .unwrap(),
633        );
634
635        let traced = TracedConfig::new(config, traces);
636
637        let paths: Vec<&str> = traced.paths().collect();
638        assert_eq!(paths.len(), 2);
639        assert!(paths.contains(&"a"));
640        assert!(paths.contains(&"b"));
641    }
642}