1use serde::Serialize;
9
10pub const FAILURE_LOG_TAIL_LINES: usize = 80;
13
14#[derive(Debug, Clone, Default, Serialize, PartialEq, Eq)]
16pub struct ErrorContext {
17 pub service: Option<String>,
18 pub hook: Option<String>,
19 pub command: Option<String>,
20 pub source_dir: Option<String>,
21 pub log_path: Option<String>,
22 pub log_hint: Option<String>,
23 pub exit_status: Option<String>,
24 pub log_tail: Option<String>,
25}
26
27pub mod codes {
33 pub const DEF_PARSE_SYNTAX: &str = "def.parse.syntax";
34 pub const DEF_PARSE_SCHEMA: &str = "def.parse.schema";
35 pub const DEF_NAME_INVALID: &str = "def.validate.name_invalid";
36 pub const DEF_NO_SERVICES: &str = "def.validate.no_services";
37 pub const DEF_UNKNOWN_KEY: &str = "def.validate.unknown_key";
38 pub const DEF_DEPENDS_ON_REJECTED: &str = "def.validate.depends_on_rejected";
39 pub const DEF_SUBSTRATE_BLOCK_INVALID: &str = "def.validate.substrate_block_invalid";
40 pub const DEF_SUBSTRATE_CONFIG_MISSING: &str = "def.validate.substrate_config_missing";
41 pub const DEF_ROOT_ORIGIN_CONFLICT: &str = "def.validate.root_origin_conflict";
42 pub const DEF_REFERENCE_SYNTAX: &str = "def.validate.reference_syntax";
43 pub const DEF_UNDECLARED_REFERENCE: &str = "def.validate.undeclared_reference";
44 pub const DEF_SECRET_NOT_REQUIRED: &str = "def.validate.secret_not_required";
45 pub const DEF_INTEGRATION_INVALID: &str = "def.validate.integration_invalid";
46 pub const DEF_WIRING_CYCLE: &str = "def.validate.wiring_cycle";
47 pub const DEF_ENV_NOT_STRINGS: &str = "def.validate.env_not_strings";
48 pub const CLI_FILE_READ: &str = "cli.file.read";
49 pub const CLI_FILE_MISSING: &str = "cli.file.missing";
50 pub const CLI_SUBSTRATE_UNKNOWN: &str = "cli.substrate.unknown";
51 pub const STATE_OPEN: &str = "state.open_failed";
52 pub const STATE_MIGRATE: &str = "state.migrate_failed";
53 pub const STATE_QUERY: &str = "state.query_failed";
54 pub const STATE_INSTANCE_EXISTS: &str = "state.instance.exists";
55 pub const STATE_INSTANCE_NOT_FOUND: &str = "state.instance.not_found";
56 pub const STATE_LOCK_HELD: &str = "state.lock.held";
57 pub const STATE_GC_FAILED: &str = "state.gc_failed";
58 pub const STATE_REMOTE_OPEN: &str = "state.remote.open_failed";
59 pub const STATE_REMOTE_QUERY: &str = "state.remote.query_failed";
60 pub const STATE_REMOTE_RUNTIME: &str = "state.remote.runtime_failed";
61 pub const STATE_REMOTE_WORKER: &str = "state.remote.worker_gone";
62 pub const STATE_ROW_DECODE: &str = "state.row.decode_failed";
63 pub const ENGINE_SUBSTRATE_MISMATCH: &str = "engine.substrate.mismatch";
64 pub const ENGINE_SUBSTRATE_REQUIRED: &str = "engine.substrate.required";
65 pub const ENGINE_SOURCE_OVERRIDE_UNSUPPORTED: &str = "engine.source_override.unsupported";
66 pub const ENGINE_SOURCE_OVERRIDE_SHARED: &str = "engine.source_override.shared";
67 pub const ENGINE_STEP_FAILED: &str = "engine.step.failed";
68 pub const ENGINE_TEARDOWN_SURVIVORS: &str = "engine.teardown.survivors";
69 pub const DAEMON_UNREACHABLE: &str = "daemon.unreachable";
70 pub const DAEMON_REQUEST_FAILED: &str = "daemon.request_failed";
71 pub const DAEMON_SPAWN_FAILED: &str = "daemon.spawn_failed";
72 pub const DAEMON_BINARY_NOT_FOUND: &str = "daemon.binary_not_found";
73 pub const CLI_RUNTIME: &str = "cli.runtime";
74 pub const LOCAL_MATERIALIZE_UNAVAILABLE: &str = "local.materialize.unavailable";
75 pub const LOCAL_SOURCE_PATH_INVALID: &str = "local.source_path.invalid";
76 pub const LOCAL_CONFIG_INVALID: &str = "local.config.invalid";
77 pub const LOCAL_PORT_ALLOC: &str = "local.port.alloc_failed";
78 pub const LOCAL_LOG_FILE: &str = "local.log_file";
79 pub const LOCAL_SPAWN_FAILED: &str = "local.spawn_failed";
80 pub const LOCAL_HOOK_FAILED: &str = "local.hook_failed";
81 pub const LOCAL_HEALTH_FAILED: &str = "local.health_failed";
82 pub const LOCAL_SERVICE_DIED: &str = "local.service_died";
83 pub const LOCAL_ENV_RESOLVE: &str = "local.env_resolve";
84 pub const LOCAL_KILL_FAILED: &str = "local.kill_failed";
85 pub const LOCAL_GIT_CLONE_FAILED: &str = "local.git.clone_failed";
86 pub const LOCAL_GIT_FETCH_FAILED: &str = "local.git.fetch_failed";
87 pub const LOCAL_GIT_REF_NOT_FOUND: &str = "local.git.ref_not_found";
88 pub const LOCAL_GIT_CHECKOUT_FAILED: &str = "local.git.checkout_failed";
89 pub const CLI_BAD_ARGUMENT: &str = "cli.bad_argument";
90 pub const CLI_FILE_WRITE: &str = "cli.file.write";
91 pub const CLI_INIT_EXISTS: &str = "cli.init.exists";
92 pub const CLI_INIT_NAME_INVALID: &str = "cli.init.name_invalid";
93 pub const CLI_ADOPT_EXISTS: &str = "cli.adopt.exists";
94 pub const CLI_ADOPT_INSPECT: &str = "cli.adopt.inspect";
95 pub const CLI_SELF_UPDATE: &str = "cli.self_update";
96 pub const DOCTOR_CHECKS_FAILED: &str = "doctor.checks.failed";
97 pub const LOCAL_DOCKER_ENGINE: &str = "local.docker.engine";
99 pub const LOCAL_DATASTORE_FAILED: &str = "local.datastore.failed";
100 pub const SECRETS_UNRESOLVED: &str = "secrets.unresolved";
101 pub const VERIFY_FAILED: &str = "verify.failed";
102 pub const VERIFY_NOT_DECLARED: &str = "verify.not_declared";
103 pub const VERIFY_SOURCE_UNAVAILABLE: &str = "verify.source_unavailable";
104 pub const VERIFY_TIER_UNKNOWN: &str = "verify.tier_unknown";
105 pub const VERIFY_TIER_REQUIRED: &str = "verify.tier_required";
106 pub const STRIPE_PROJECTS_UNAVAILABLE: &str = "stripe.projects.unavailable";
107 pub const STRIPE_PROJECTS_AUTH: &str = "stripe.projects.auth";
108 pub const STRIPE_PROJECTS_FAILED: &str = "stripe.projects.failed";
109 pub const STRIPE_PROJECTS_LOCK_HELD: &str = "stripe.projects.lock_held";
110 pub const STRIPE_PROJECTS_TIMEOUT: &str = "stripe.projects.timeout";
111 pub const STRIPE_PROJECT_ANCHOR: &str = "stripe.project.anchor";
112 pub const STRIPE_PROJECTS_PROVISION_FAILED: &str = "stripe.projects.provision_failed";
113 pub const STRIPE_PROJECTS_CATALOG_MISSING: &str = "stripe.projects.catalog_missing";
114 pub const STRIPE_PROJECTS_CONFIG_SCHEMA: &str = "stripe.projects.config_schema";
115 pub const INTEGRATION_CONFIG_INVALID: &str = "integration.config.invalid";
116 pub const INTEGRATION_HOST_UNSUPPORTED: &str = "integration.host.unsupported";
117
118 pub const ALL: &[&str] = &[
122 DEF_PARSE_SYNTAX,
123 DEF_PARSE_SCHEMA,
124 DEF_NAME_INVALID,
125 DEF_NO_SERVICES,
126 DEF_UNKNOWN_KEY,
127 DEF_DEPENDS_ON_REJECTED,
128 DEF_SUBSTRATE_BLOCK_INVALID,
129 DEF_SUBSTRATE_CONFIG_MISSING,
130 DEF_ROOT_ORIGIN_CONFLICT,
131 DEF_REFERENCE_SYNTAX,
132 DEF_UNDECLARED_REFERENCE,
133 DEF_SECRET_NOT_REQUIRED,
134 DEF_INTEGRATION_INVALID,
135 DEF_WIRING_CYCLE,
136 DEF_ENV_NOT_STRINGS,
137 CLI_FILE_READ,
138 CLI_FILE_MISSING,
139 CLI_SUBSTRATE_UNKNOWN,
140 STATE_OPEN,
141 STATE_MIGRATE,
142 STATE_QUERY,
143 STATE_INSTANCE_EXISTS,
144 STATE_INSTANCE_NOT_FOUND,
145 STATE_LOCK_HELD,
146 STATE_GC_FAILED,
147 STATE_REMOTE_OPEN,
148 STATE_REMOTE_QUERY,
149 STATE_REMOTE_RUNTIME,
150 STATE_REMOTE_WORKER,
151 STATE_ROW_DECODE,
152 ENGINE_SUBSTRATE_MISMATCH,
153 ENGINE_SUBSTRATE_REQUIRED,
154 ENGINE_SOURCE_OVERRIDE_UNSUPPORTED,
155 ENGINE_SOURCE_OVERRIDE_SHARED,
156 ENGINE_STEP_FAILED,
157 ENGINE_TEARDOWN_SURVIVORS,
158 DAEMON_UNREACHABLE,
159 DAEMON_REQUEST_FAILED,
160 DAEMON_SPAWN_FAILED,
161 DAEMON_BINARY_NOT_FOUND,
162 CLI_RUNTIME,
163 LOCAL_MATERIALIZE_UNAVAILABLE,
164 LOCAL_SOURCE_PATH_INVALID,
165 LOCAL_CONFIG_INVALID,
166 LOCAL_PORT_ALLOC,
167 LOCAL_LOG_FILE,
168 LOCAL_SPAWN_FAILED,
169 LOCAL_HOOK_FAILED,
170 LOCAL_HEALTH_FAILED,
171 LOCAL_SERVICE_DIED,
172 LOCAL_ENV_RESOLVE,
173 LOCAL_KILL_FAILED,
174 LOCAL_GIT_CLONE_FAILED,
175 LOCAL_GIT_FETCH_FAILED,
176 LOCAL_GIT_REF_NOT_FOUND,
177 LOCAL_GIT_CHECKOUT_FAILED,
178 CLI_BAD_ARGUMENT,
179 CLI_FILE_WRITE,
180 CLI_INIT_EXISTS,
181 CLI_INIT_NAME_INVALID,
182 CLI_ADOPT_EXISTS,
183 CLI_ADOPT_INSPECT,
184 CLI_SELF_UPDATE,
185 DOCTOR_CHECKS_FAILED,
186 LOCAL_DOCKER_ENGINE,
187 LOCAL_DATASTORE_FAILED,
188 SECRETS_UNRESOLVED,
189 VERIFY_FAILED,
190 VERIFY_NOT_DECLARED,
191 VERIFY_SOURCE_UNAVAILABLE,
192 VERIFY_TIER_UNKNOWN,
193 VERIFY_TIER_REQUIRED,
194 STRIPE_PROJECTS_UNAVAILABLE,
195 STRIPE_PROJECTS_AUTH,
196 STRIPE_PROJECTS_FAILED,
197 STRIPE_PROJECTS_LOCK_HELD,
198 STRIPE_PROJECTS_TIMEOUT,
199 STRIPE_PROJECT_ANCHOR,
200 STRIPE_PROJECTS_PROVISION_FAILED,
201 STRIPE_PROJECTS_CATALOG_MISSING,
202 STRIPE_PROJECTS_CONFIG_SCHEMA,
203 INTEGRATION_CONFIG_INVALID,
204 INTEGRATION_HOST_UNSUPPORTED,
205 ];
206}
207
208pub trait Fault: std::error::Error {
213 fn code(&self) -> &'static str;
215 fn remediation(&self) -> String;
217 fn step(&self) -> Option<&str> {
219 None
220 }
221 fn instance(&self) -> Option<&str> {
223 None
224 }
225 fn context(&self) -> ErrorContext {
227 ErrorContext::default()
228 }
229}
230
231#[derive(Debug, Serialize)]
233pub struct Report {
234 pub schema_version: u32,
235 pub code: &'static str,
236 pub message: String,
237 #[serde(skip_serializing_if = "Option::is_none")]
238 pub step: Option<String>,
239 #[serde(skip_serializing_if = "Option::is_none")]
240 pub instance: Option<String>,
241 pub remediation: String,
242 pub context: ErrorContext,
243}
244
245impl Report {
246 pub fn from_fault(fault: &dyn Fault) -> Self {
247 Self {
248 schema_version: 1,
249 code: fault.code(),
250 message: fault.to_string(),
251 step: fault.step().map(str::to_owned),
252 instance: fault.instance().map(str::to_owned),
253 remediation: fault.remediation(),
254 context: fault.context(),
255 }
256 }
257}
258
259#[cfg(test)]
260mod tests {
261 use super::{ErrorContext, Report, codes};
262 use crate::substrate::SubstrateFault;
263 use std::collections::BTreeSet;
264
265 #[test]
266 fn codes_are_unique() {
267 let set: BTreeSet<_> = codes::ALL.iter().collect();
268 assert_eq!(set.len(), codes::ALL.len());
269 }
270
271 #[test]
272 fn report_serializes_schema_version_and_context() {
273 let fault = SubstrateFault {
274 code: codes::LOCAL_HOOK_FAILED,
275 message: "setup hook exited with exit status: 1".into(),
276 remediation: "re-run up".into(),
277 context: Box::new(ErrorContext {
278 service: Some("web".into()),
279 hook: Some("setup".into()),
280 command: Some("mise install".into()),
281 source_dir: Some("/tmp/web".into()),
282 log_path: Some("/tmp/logs/web.log".into()),
283 exit_status: Some("exit status: 1".into()),
284 log_tail: Some("error: trust denied".into()),
285 ..ErrorContext::default()
286 }),
287 };
288 let report = Report::from_fault(&fault);
289 assert_eq!(report.schema_version, 1);
290 assert_eq!(report.context.service.as_deref(), Some("web"));
291 let json = serde_json::to_value(&report).unwrap();
292 assert_eq!(json["schema_version"], 1);
293 assert_eq!(json["context"]["command"], "mise install");
294 assert_eq!(json["context"]["log_tail"], "error: trust denied");
295 }
296}