myc

Self-custodial remote signer for Radroots apps
git clone https://radroots.dev/git/myc.git
Log | Files | Refs | README | LICENSE

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 }