Skip to main content

sword_core/error/
diagnostic.rs

1#[derive(Debug, Clone)]
2pub struct StartupDiagnostic {
3    title: String,
4    reason: String,
5    source: Option<String>,
6    context: Vec<(String, String)>,
7    hints: Vec<String>,
8}
9
10impl StartupDiagnostic {
11    pub fn new(title: String, reason: String) -> Self {
12        Self {
13            title,
14            reason,
15            source: None,
16            context: Vec::new(),
17            hints: Vec::new(),
18        }
19    }
20
21    pub fn with_source(mut self, source: String) -> Self {
22        self.source = Some(source);
23        self
24    }
25
26    pub fn add_context(mut self, key: String, value: String) -> Self {
27        self.context.push((key, value));
28        self
29    }
30
31    pub fn extend_context<I, K, V>(mut self, context: I) -> Self
32    where
33        I: IntoIterator<Item = (K, V)>,
34        K: Into<String>,
35        V: Into<String>,
36    {
37        self.context.extend(
38            context
39                .into_iter()
40                .map(|(key, value)| (key.into(), value.into())),
41        );
42
43        self
44    }
45
46    pub fn add_hint(mut self, hint: String) -> Self {
47        self.hints.push(hint);
48        self
49    }
50
51    fn has_details(&self) -> bool {
52        !self.context.is_empty() || !self.hints.is_empty()
53    }
54}
55
56pub fn emit(diagnostic: StartupDiagnostic) {
57    if tracing::dispatcher::has_been_set() {
58        emit_tracing_logs(&diagnostic);
59    } else {
60        eprintln!("ERROR: {}", diagnostic.title);
61        eprintln!("Reason: {}", diagnostic.reason);
62        eprintln!(
63            "Source: {}",
64            diagnostic.source.as_deref().unwrap_or("Unknown")
65        );
66        eprintln!("Context:");
67        for (key, value) in &diagnostic.context {
68            eprintln!("  {}: {}", key, value);
69        }
70        eprintln!("Hints:");
71        for hint in &diagnostic.hints {
72            eprintln!("  {}", hint);
73        }
74        eprintln!("Enable tracing in config to see details");
75    }
76}
77
78fn emit_tracing_logs(diagnostic: &StartupDiagnostic) {
79    if let Some(source) = diagnostic.source.as_deref() {
80        tracing::error!(
81            target: "sword.startup.error",
82            source,
83            reason = %diagnostic.reason,
84            "{}",
85            diagnostic.title
86        );
87
88        if diagnostic.has_details() {
89            emit_context_summary_error(Some(source), &diagnostic.context);
90
91            for (key, value) in &diagnostic.context {
92                tracing::debug!(
93                    target: "sword.startup.error",
94                    source,
95                    context_key = %key,
96                    context_value = %value,
97                    "Startup diagnostic context"
98                );
99            }
100
101            for hint in &diagnostic.hints {
102                tracing::debug!(
103                    target: "sword.startup.error",
104                    source,
105                    hint = %hint,
106                    "Startup diagnostic hint"
107                );
108            }
109        }
110
111        return;
112    }
113
114    tracing::error!(
115        target: "sword.startup.error",
116        reason = %diagnostic.reason,
117        "{}",
118        diagnostic.title
119    );
120
121    if diagnostic.has_details() {
122        emit_context_summary_error(None, &diagnostic.context);
123
124        for (key, value) in &diagnostic.context {
125            tracing::debug!(
126                target: "sword.startup.error",
127                context_key = %key,
128                context_value = %value,
129                "Startup diagnostic context"
130            );
131        }
132
133        for hint in &diagnostic.hints {
134            tracing::debug!(
135                target: "sword.startup.error",
136                hint = %hint,
137                "Startup diagnostic hint"
138            );
139        }
140    }
141}
142
143fn emit_context_summary_error(source: Option<&str>, context: &[(String, String)]) {
144    if context.is_empty() {
145        return;
146    }
147
148    const PRIORITY_KEYS: [&str; 6] = [
149        "missing_dependency_path",
150        "dependency_path",
151        "controller_name",
152        "handler_id",
153        "path",
154        "bind",
155    ];
156
157    let mut selected: Vec<(&str, &str)> = Vec::new();
158
159    for key in PRIORITY_KEYS {
160        if let Some((found_key, found_value)) = context
161            .iter()
162            .find(|(ctx_key, _)| ctx_key.as_str() == key)
163            .map(|(ctx_key, ctx_value)| (ctx_key.as_str(), ctx_value.as_str()))
164        {
165            selected.push((found_key, found_value));
166        }
167    }
168
169    if selected.is_empty() {
170        selected.extend(
171            context
172                .iter()
173                .take(2)
174                .map(|(key, value)| (key.as_str(), value.as_str())),
175        );
176    }
177
178    for (key, value) in selected {
179        if let Some(source) = source {
180            tracing::error!(
181                target: "sword.startup.error",
182                source,
183                context_key = %key,
184                context_value = %value,
185                "Startup diagnostic key context"
186            );
187        } else {
188            tracing::error!(
189                target: "sword.startup.error",
190                context_key = %key,
191                context_value = %value,
192                "Startup diagnostic key context"
193            );
194        }
195    }
196}