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