sword_core/error/
diagnostic.rs1#[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}