refactor(firmware): centralize configuration and diagnostics
This commit is contained in:
1 parent
7048ed89d0
commit
387fdaad23
140 files changed
+6891
-4541
No files matched your search
@@ -6,15 +6,12 @@
|
||||
|
||||
use core::{
|
||||
cell::UnsafeCell,
|
||||
sync::atomic::{AtomicBool, AtomicU8, Ordering},
|
||||
sync::atomic::{AtomicBool, Ordering},
|
||||
};
|
||||
|
||||
use log::{Level, LevelFilter, Log, Metadata, Record, SetLoggerError};
|
||||
|
||||
use crate::{
|
||||
ErrorAdditionalInformation, ErrorPath, EventHub, LogFilter, LogFlags, LogText, PublishOutcome,
|
||||
Severity,
|
||||
};
|
||||
use crate::{ErrorAdditionalInformation, ErrorPath, LogFlags, LogRecord, LogText, Severity};
|
||||
|
||||
/// Saturating, process-local accounting for the optional global facade.
|
||||
///
|
||||
@@ -24,8 +21,6 @@ use crate::{
|
||||
pub struct FacadeHealth {
|
||||
/// Calls rejected before formatting because their context is forbidden.
|
||||
pub forbidden_context: u32,
|
||||
/// Optional records excluded by the active verbosity cap.
|
||||
pub filtered: u32,
|
||||
/// Calls that lost the one-attempt ingress ownership race.
|
||||
pub contended: u32,
|
||||
/// Calls dropped because their bounded ingress lane was full.
|
||||
@@ -36,7 +31,6 @@ pub struct FacadeHealth {
|
||||
|
||||
struct FacadeCounters {
|
||||
forbidden_context: core::sync::atomic::AtomicU32,
|
||||
filtered: core::sync::atomic::AtomicU32,
|
||||
contended: core::sync::atomic::AtomicU32,
|
||||
full: core::sync::atomic::AtomicU32,
|
||||
truncated: core::sync::atomic::AtomicU32,
|
||||
@@ -46,7 +40,6 @@ impl FacadeCounters {
|
||||
const fn new() -> Self {
|
||||
Self {
|
||||
forbidden_context: core::sync::atomic::AtomicU32::new(0),
|
||||
filtered: core::sync::atomic::AtomicU32::new(0),
|
||||
contended: core::sync::atomic::AtomicU32::new(0),
|
||||
full: core::sync::atomic::AtomicU32::new(0),
|
||||
truncated: core::sync::atomic::AtomicU32::new(0),
|
||||
@@ -71,7 +64,6 @@ impl FacadeCounters {
|
||||
fn snapshot(&self) -> FacadeHealth {
|
||||
FacadeHealth {
|
||||
forbidden_context: self.forbidden_context.load(Ordering::Relaxed),
|
||||
filtered: self.filtered.load(Ordering::Relaxed),
|
||||
contended: self.contended.load(Ordering::Relaxed),
|
||||
full: self.full.load(Ordering::Relaxed),
|
||||
truncated: self.truncated.load(Ordering::Relaxed),
|
||||
@@ -146,8 +138,8 @@ impl<const N: usize> IngressQueue<N> {
|
||||
}
|
||||
}
|
||||
|
||||
/// Fixed global-facade ingress. The normal task/runtime owner drains it into
|
||||
/// an [`EventHub`] after startup.
|
||||
/// Fixed global-facade ingress. The normal task/runtime owner takes each
|
||||
/// transport-neutral record and fans it out to independently filtered sinks.
|
||||
pub struct FacadeIngress<const N: usize> {
|
||||
/// One dedicated critical slot keeps error records independent from the
|
||||
/// bounded ordinary FIFO. It is deliberately one slot: producers never
|
||||
@@ -157,7 +149,6 @@ pub struct FacadeIngress<const N: usize> {
|
||||
critical_busy: AtomicBool,
|
||||
queue: UnsafeCell<IngressQueue<N>>,
|
||||
queue_busy: AtomicBool,
|
||||
maximum_severity: AtomicU8,
|
||||
counters: FacadeCounters,
|
||||
}
|
||||
|
||||
@@ -167,29 +158,17 @@ pub struct FacadeIngress<const N: usize> {
|
||||
unsafe impl<const N: usize> Sync for FacadeIngress<N> {}
|
||||
|
||||
impl<const N: usize> FacadeIngress<N> {
|
||||
/// Creates empty fixed ingress with the boot-active verbosity cap.
|
||||
pub const fn new(filter: LogFilter) -> Self {
|
||||
/// Creates an empty fixed ingress.
|
||||
pub const fn new() -> Self {
|
||||
Self {
|
||||
critical: UnsafeCell::new(None),
|
||||
critical_busy: AtomicBool::new(false),
|
||||
queue: UnsafeCell::new(IngressQueue::new()),
|
||||
queue_busy: AtomicBool::new(false),
|
||||
maximum_severity: AtomicU8::new(filter.maximum_severity() as u8),
|
||||
counters: FacadeCounters::new(),
|
||||
}
|
||||
}
|
||||
|
||||
/// Updates the runtime-selected maximum non-error verbosity.
|
||||
pub fn set_filter(&self, filter: LogFilter) {
|
||||
self.maximum_severity
|
||||
.store(filter.maximum_severity() as u8, Ordering::Release);
|
||||
}
|
||||
|
||||
fn allows(&self, severity: Severity) -> bool {
|
||||
severity <= Severity::Error
|
||||
|| (severity as u8) <= self.maximum_severity.load(Ordering::Acquire)
|
||||
}
|
||||
|
||||
fn push(&self, record: IngressRecord) {
|
||||
if record.severity <= Severity::Error {
|
||||
self.push_critical(record);
|
||||
@@ -222,24 +201,23 @@ impl<const N: usize> FacadeIngress<N> {
|
||||
self.counters.snapshot()
|
||||
}
|
||||
|
||||
/// Moves at most one preformatted facade record into the central hub.
|
||||
pub fn drain_one_into<const CRITICAL: usize, const BEST_EFFORT: usize>(
|
||||
&self,
|
||||
hub: &mut EventHub<CRITICAL, BEST_EFFORT>,
|
||||
) -> Option<PublishOutcome> {
|
||||
/// Takes at most one preformatted event for composition-level fan-out.
|
||||
///
|
||||
/// The ingress deliberately applies no sink policy. A caller may submit
|
||||
/// the returned copy to MAVLink, blackbox, or any later sink without one
|
||||
/// sink's verbosity selection suppressing another's event.
|
||||
pub fn take(&self) -> Option<LogRecord> {
|
||||
let record = match self.with_critical(Option::take) {
|
||||
Some(Some(record)) => record,
|
||||
Some(None) => self.with_queue(IngressQueue::pop).flatten()?,
|
||||
None => return None,
|
||||
};
|
||||
Some(match record.body {
|
||||
IngressBody::Log { message } => {
|
||||
hub.publish_text(record.severity, record.flags, message)
|
||||
}
|
||||
IngressBody::Log { message } => LogRecord::new(record.severity, record.flags, message),
|
||||
IngressBody::Error {
|
||||
path,
|
||||
additional_information,
|
||||
} => hub.publish_error_text(record.flags, path, additional_information),
|
||||
} => LogRecord::error(record.flags, path, additional_information),
|
||||
})
|
||||
}
|
||||
|
||||
@@ -278,6 +256,12 @@ impl<const N: usize> FacadeIngress<N> {
|
||||
}
|
||||
}
|
||||
|
||||
impl<const N: usize> Default for FacadeIngress<N> {
|
||||
fn default() -> Self {
|
||||
Self::new()
|
||||
}
|
||||
}
|
||||
|
||||
/// Installed standard logger. The context probe must return true for
|
||||
/// ISR/panic/hard-fault/shutdown contexts; those calls are rejected before
|
||||
/// [`Record::args`] is formatted.
|
||||
@@ -296,7 +280,8 @@ impl<const N: usize> GlobalLogger<N> {
|
||||
}
|
||||
|
||||
/// Installs this logger once for the process/image and enables all Rust
|
||||
/// levels compiled into the image. Kaptan's own filter applies afterward.
|
||||
/// levels compiled into the image. Each sink applies its policy after it
|
||||
/// receives a record from the ingress.
|
||||
pub fn install(&'static self) -> Result<(), SetLoggerError> {
|
||||
log::set_logger(self)?;
|
||||
log::set_max_level(LevelFilter::Trace);
|
||||
@@ -305,18 +290,11 @@ impl<const N: usize> GlobalLogger<N> {
|
||||
}
|
||||
|
||||
impl<const N: usize> Log for GlobalLogger<N> {
|
||||
fn enabled(&self, metadata: &Metadata<'_>) -> bool {
|
||||
fn enabled(&self, _: &Metadata<'_>) -> bool {
|
||||
if (self.forbidden_context)() {
|
||||
FacadeCounters::increment(&self.ingress.counters.forbidden_context);
|
||||
return false;
|
||||
}
|
||||
let Some(severity) = severity(metadata.level()) else {
|
||||
return false;
|
||||
};
|
||||
if !self.ingress.allows(severity) {
|
||||
FacadeCounters::increment(&self.ingress.counters.filtered);
|
||||
return false;
|
||||
}
|
||||
true
|
||||
}
|
||||
|
||||
@@ -325,13 +303,7 @@ impl<const N: usize> Log for GlobalLogger<N> {
|
||||
FacadeCounters::increment(&self.ingress.counters.forbidden_context);
|
||||
return;
|
||||
}
|
||||
let Some(severity) = severity(record.level()) else {
|
||||
return;
|
||||
};
|
||||
if !self.ingress.allows(severity) {
|
||||
FacadeCounters::increment(&self.ingress.counters.filtered);
|
||||
return;
|
||||
}
|
||||
let severity = severity(record.level());
|
||||
let flags = if record.level() == Level::Trace {
|
||||
LogFlags::TRACE
|
||||
} else {
|
||||
@@ -367,12 +339,12 @@ impl<const N: usize> Log for GlobalLogger<N> {
|
||||
fn flush(&self) {}
|
||||
}
|
||||
|
||||
const fn severity(level: Level) -> Option<Severity> {
|
||||
const fn severity(level: Level) -> Severity {
|
||||
match level {
|
||||
Level::Error => Some(Severity::Error),
|
||||
Level::Warn => Some(Severity::Warning),
|
||||
Level::Info => Some(Severity::Info),
|
||||
Level::Debug | Level::Trace => Some(Severity::Debug),
|
||||
Level::Error => Severity::Error,
|
||||
Level::Warn => Severity::Warning,
|
||||
Level::Info => Severity::Info,
|
||||
Level::Debug | Level::Trace => Severity::Debug,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -382,10 +354,10 @@ mod tests {
|
||||
use std::sync::Once;
|
||||
|
||||
use super::{FacadeIngress, GlobalLogger};
|
||||
use crate::{EventHub, LogFilter, RecordChannel, Severity};
|
||||
use crate::{RecordChannel, Severity};
|
||||
use log::{Level, Log, Metadata, Record};
|
||||
|
||||
static GLOBAL_INGRESS: FacadeIngress<2> = FacadeIngress::new(LogFilter::new(Severity::Debug));
|
||||
static GLOBAL_INGRESS: FacadeIngress<2> = FacadeIngress::new();
|
||||
static GLOBAL_LOGGER: GlobalLogger<2> = GlobalLogger::new(&GLOBAL_INGRESS, never_forbidden);
|
||||
static INSTALL_GLOBAL: Once = Once::new();
|
||||
|
||||
@@ -399,7 +371,7 @@ mod tests {
|
||||
|
||||
#[test]
|
||||
fn rejects_forbidden_context_before_formatting() {
|
||||
static INGRESS: FacadeIngress<2> = FacadeIngress::new(LogFilter::new(Severity::Debug));
|
||||
static INGRESS: FacadeIngress<2> = FacadeIngress::new();
|
||||
let logger = GlobalLogger::new(&INGRESS, always_forbidden);
|
||||
let metadata = Metadata::builder()
|
||||
.level(Level::Error)
|
||||
@@ -408,19 +380,31 @@ mod tests {
|
||||
assert!(!logger.enabled(&metadata));
|
||||
assert_eq!(INGRESS.health().forbidden_context, 1);
|
||||
|
||||
static FILTERED: FacadeIngress<2> = FacadeIngress::new(LogFilter::new(Severity::Info));
|
||||
let filtered_logger = GlobalLogger::new(&FILTERED, never_forbidden);
|
||||
static UNFILTERED: FacadeIngress<2> = FacadeIngress::new();
|
||||
let unfiltered_logger = GlobalLogger::new(&UNFILTERED, never_forbidden);
|
||||
let debug = Metadata::builder()
|
||||
.level(Level::Debug)
|
||||
.target("test")
|
||||
.build();
|
||||
assert!(!filtered_logger.enabled(&debug));
|
||||
assert_eq!(FILTERED.health().filtered, 1);
|
||||
assert!(unfiltered_logger.enabled(&debug));
|
||||
unfiltered_logger.log(
|
||||
&Record::builder()
|
||||
.metadata(debug)
|
||||
.args(format_args!("sink decides whether to keep this"))
|
||||
.build(),
|
||||
);
|
||||
assert_eq!(
|
||||
UNFILTERED
|
||||
.take()
|
||||
.expect("debug event reaches the unfiltered ingress")
|
||||
.severity(),
|
||||
Severity::Debug
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn formats_once_then_drains_into_priority_hub() {
|
||||
static INGRESS: FacadeIngress<2> = FacadeIngress::new(LogFilter::new(Severity::Debug));
|
||||
fn formats_once_then_exposes_a_transport_neutral_record() {
|
||||
static INGRESS: FacadeIngress<2> = FacadeIngress::new();
|
||||
let logger = GlobalLogger::new(&INGRESS, never_forbidden);
|
||||
let record = Record::builder()
|
||||
.metadata(
|
||||
@@ -432,9 +416,7 @@ mod tests {
|
||||
.args(format_args!("link failed"))
|
||||
.build();
|
||||
logger.log(&record);
|
||||
let mut hub = EventHub::<1, 1>::new(LogFilter::default());
|
||||
assert!(INGRESS.drain_one_into(&mut hub).is_some());
|
||||
let pending = hub.peek().unwrap();
|
||||
let pending = INGRESS.take().expect("facade event retained");
|
||||
assert_eq!(pending.error_path().unwrap().as_str(), "test");
|
||||
assert_eq!(
|
||||
pending.additional_information().unwrap().as_str(),
|
||||
@@ -456,11 +438,10 @@ mod tests {
|
||||
fn standard_log_macros_enqueue_bounded_targeted_text() {
|
||||
INSTALL_GLOBAL.call_once(|| GLOBAL_LOGGER.install().expect("one global test logger"));
|
||||
log::info!("armed configuration loaded");
|
||||
let mut hub = EventHub::<1, 2>::new(LogFilter::default());
|
||||
assert!(GLOBAL_INGRESS.drain_one_into(&mut hub).is_some());
|
||||
assert!(
|
||||
hub.peek()
|
||||
.unwrap()
|
||||
GLOBAL_INGRESS
|
||||
.take()
|
||||
.expect("macro event retained")
|
||||
.message()
|
||||
.unwrap()
|
||||
.as_str()
|
||||
@@ -470,7 +451,7 @@ mod tests {
|
||||
|
||||
#[test]
|
||||
fn forbidden_context_skips_argument_formatting() {
|
||||
static INGRESS: FacadeIngress<2> = FacadeIngress::new(LogFilter::new(Severity::Debug));
|
||||
static INGRESS: FacadeIngress<2> = FacadeIngress::new();
|
||||
let logger = GlobalLogger::new(&INGRESS, always_forbidden);
|
||||
logger.log(
|
||||
&Record::builder()
|
||||
@@ -488,7 +469,7 @@ mod tests {
|
||||
|
||||
#[test]
|
||||
fn bounded_ingress_exposes_local_drop_and_truncation_accounting() {
|
||||
static INGRESS: FacadeIngress<1> = FacadeIngress::new(LogFilter::new(Severity::Info));
|
||||
static INGRESS: FacadeIngress<1> = FacadeIngress::new();
|
||||
let logger = GlobalLogger::new(&INGRESS, never_forbidden);
|
||||
logger.log(
|
||||
&Record::builder()
|
||||
@@ -531,7 +512,7 @@ mod tests {
|
||||
|
||||
#[test]
|
||||
fn full_optional_ingress_retains_an_error_for_the_error_channel() {
|
||||
static INGRESS: FacadeIngress<1> = FacadeIngress::new(LogFilter::new(Severity::Info));
|
||||
static INGRESS: FacadeIngress<1> = FacadeIngress::new();
|
||||
let logger = GlobalLogger::new(&INGRESS, never_forbidden);
|
||||
logger.log(
|
||||
&Record::builder()
|
||||
@@ -556,9 +537,9 @@ mod tests {
|
||||
.build(),
|
||||
);
|
||||
|
||||
let mut hub = EventHub::<1, 1>::new(LogFilter::default());
|
||||
assert!(INGRESS.drain_one_into(&mut hub).is_some());
|
||||
let error = hub.peek().expect("error retained ahead of optional text");
|
||||
let error = INGRESS
|
||||
.take()
|
||||
.expect("error retained ahead of optional text");
|
||||
assert_eq!(error.channel(), RecordChannel::Error);
|
||||
assert_eq!(error.error_path().unwrap().as_str(), "test");
|
||||
assert_eq!(error.additional_information().unwrap().as_str(), "critical");
|
||||
|
||||
Reference in new issue
Block a user