diagnostics_v1.rs (7163B)
1 //! Closed process-result and structured stderr diagnostic contract. 2 3 use core::fmt; 4 use std::process::ExitCode; 5 6 use crate::MycServicePhase; 7 8 /// Exact Myc diagnostics contract version. 9 pub const MYC_DIAGNOSTICS_CONTRACT_VERSION: u32 = 1; 10 11 /// Hard maximum for one canonical Myc structured log record, excluding newline. 12 pub const MYC_LOG_RECORD_MAX_UTF8_BYTES: usize = 512; 13 14 const LOG_SCHEMA: &str = "radroots.myc.log.v1"; 15 16 /// Exact stable Myc process result and exit-code inventory. 17 #[derive(Clone, Copy, Debug, PartialEq, Eq)] 18 pub enum MycProcessResult { 19 Success, 20 UnexpectedInternal, 21 InputOrConfiguration, 22 ServiceOrDependencyUnavailable, 23 StateOrIdentityUnavailable, 24 OperationRejectedOrConflict, 25 DoctorRequiredCheckFailed, 26 } 27 28 impl MycProcessResult { 29 #[must_use] 30 pub const fn exit_code_u8(self) -> u8 { 31 match self { 32 Self::Success => 0, 33 Self::UnexpectedInternal => 1, 34 Self::InputOrConfiguration => 2, 35 Self::ServiceOrDependencyUnavailable => 3, 36 Self::StateOrIdentityUnavailable => 4, 37 Self::OperationRejectedOrConflict => 5, 38 Self::DoctorRequiredCheckFailed => 6, 39 } 40 } 41 42 #[must_use] 43 pub fn exit_code(self) -> ExitCode { 44 ExitCode::from(self.exit_code_u8()) 45 } 46 47 #[must_use] 48 pub const fn code(self) -> &'static str { 49 match self { 50 Self::Success => "success", 51 Self::UnexpectedInternal => "unexpected_internal", 52 Self::InputOrConfiguration => "input_or_configuration", 53 Self::ServiceOrDependencyUnavailable => "service_or_dependency_unavailable", 54 Self::StateOrIdentityUnavailable => "state_or_identity_unavailable", 55 Self::OperationRejectedOrConflict => "operation_rejected_or_conflict", 56 Self::DoctorRequiredCheckFailed => "doctor_required_check_failed", 57 } 58 } 59 } 60 61 /// Closed structured-log severity vocabulary admitted by Myc. 62 #[derive(Clone, Copy, Debug, PartialEq, Eq)] 63 pub enum MycLogLevel { 64 Trace, 65 Debug, 66 Info, 67 Warn, 68 Error, 69 } 70 71 impl MycLogLevel { 72 #[must_use] 73 pub const fn as_str(self) -> &'static str { 74 match self { 75 Self::Trace => "trace", 76 Self::Debug => "debug", 77 Self::Info => "info", 78 Self::Warn => "warn", 79 Self::Error => "error", 80 } 81 } 82 } 83 84 /// Closed structured-log event vocabulary required by the current runtime plan. 85 #[derive(Clone, Copy, Debug, PartialEq, Eq)] 86 pub enum MycLogEvent { 87 ProcessResult, 88 Lifecycle, 89 CriticalTaskFailed, 90 ShutdownRequested, 91 ShutdownForced, 92 } 93 94 impl MycLogEvent { 95 #[must_use] 96 pub const fn as_str(self) -> &'static str { 97 match self { 98 Self::ProcessResult => "process_result", 99 Self::Lifecycle => "lifecycle", 100 Self::CriticalTaskFailed => "critical_task_failed", 101 Self::ShutdownRequested => "shutdown_requested", 102 Self::ShutdownForced => "shutdown_forced", 103 } 104 } 105 } 106 107 /// One sealed canonical structured log record containing only governed values. 108 #[derive(Clone, Copy, PartialEq, Eq)] 109 pub struct MycLogRecord { 110 level: MycLogLevel, 111 event: MycLogEvent, 112 code: &'static str, 113 process_result: Option<MycProcessResult>, 114 } 115 116 impl MycLogRecord { 117 /// Builds the exact terminal record for one governed process result. 118 #[must_use] 119 pub const fn process_result(result: MycProcessResult) -> Self { 120 let level = match result { 121 MycProcessResult::Success => MycLogLevel::Info, 122 MycProcessResult::OperationRejectedOrConflict => MycLogLevel::Warn, 123 MycProcessResult::UnexpectedInternal 124 | MycProcessResult::InputOrConfiguration 125 | MycProcessResult::ServiceOrDependencyUnavailable 126 | MycProcessResult::StateOrIdentityUnavailable 127 | MycProcessResult::DoctorRequiredCheckFailed => MycLogLevel::Error, 128 }; 129 Self { 130 level, 131 event: MycLogEvent::ProcessResult, 132 code: result.code(), 133 process_result: Some(result), 134 } 135 } 136 137 /// Builds the exact phase record from an already-published lifecycle value. 138 #[must_use] 139 pub const fn lifecycle(phase: MycServicePhase) -> Self { 140 let (level, code) = match phase { 141 MycServicePhase::Starting => (MycLogLevel::Info, "starting"), 142 MycServicePhase::Ready => (MycLogLevel::Info, "ready"), 143 MycServicePhase::Degraded => (MycLogLevel::Warn, "degraded"), 144 MycServicePhase::Unready => (MycLogLevel::Warn, "unready"), 145 MycServicePhase::Stopping => (MycLogLevel::Info, "stopping"), 146 MycServicePhase::Failed => (MycLogLevel::Error, "failed"), 147 }; 148 Self { 149 level, 150 event: MycLogEvent::Lifecycle, 151 code, 152 process_result: None, 153 } 154 } 155 156 #[must_use] 157 pub const fn critical_task_failed() -> Self { 158 Self { 159 level: MycLogLevel::Error, 160 event: MycLogEvent::CriticalTaskFailed, 161 code: "critical_task_failed", 162 process_result: None, 163 } 164 } 165 166 #[must_use] 167 pub const fn shutdown_requested() -> Self { 168 Self { 169 level: MycLogLevel::Info, 170 event: MycLogEvent::ShutdownRequested, 171 code: "first_signal", 172 process_result: None, 173 } 174 } 175 176 #[must_use] 177 pub const fn shutdown_forced() -> Self { 178 Self { 179 level: MycLogLevel::Error, 180 event: MycLogEvent::ShutdownForced, 181 code: "second_signal", 182 process_result: None, 183 } 184 } 185 186 #[must_use] 187 pub const fn level(&self) -> MycLogLevel { 188 self.level 189 } 190 191 #[must_use] 192 pub const fn event(&self) -> MycLogEvent { 193 self.event 194 } 195 196 #[must_use] 197 pub const fn code(&self) -> &'static str { 198 self.code 199 } 200 201 #[must_use] 202 pub const fn process_exit(&self) -> Option<MycProcessResult> { 203 self.process_result 204 } 205 } 206 207 impl fmt::Debug for MycLogRecord { 208 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result { 209 formatter 210 .debug_struct("MycLogRecord") 211 .field("level", &self.level) 212 .field("event", &self.event) 213 .field("code", &self.code) 214 .field( 215 "exit_code", 216 &self.process_result.map(MycProcessResult::exit_code_u8), 217 ) 218 .finish() 219 } 220 } 221 222 impl fmt::Display for MycLogRecord { 223 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result { 224 write!( 225 formatter, 226 "{{\"schema\":\"{LOG_SCHEMA}\",\"contract_version\":{MYC_DIAGNOSTICS_CONTRACT_VERSION},\"service\":\"myc\",\"level\":\"{}\",\"event\":\"{}\",\"code\":\"{}\"", 227 self.level.as_str(), 228 self.event.as_str(), 229 self.code, 230 )?; 231 if let Some(result) = self.process_result { 232 write!(formatter, ",\"exit_code\":{}", result.exit_code_u8())?; 233 } 234 formatter.write_str("}") 235 } 236 }