use super::error_codes::NamErrorCode;
use super::format::{format_model_path, redact_path, redact_text, timestamp};
use super::logger::NamLogger;
use super::snapshot::{ACTIVE_MODEL_NAME, ACTIVE_SAMPLE_RATE, RuntimeSnapshot};
use super::system_info::SystemSnapshot;
use std::sync::atomic::Ordering;
#[derive(Debug, Clone)]
pub struct ErrorContext {
pub code: NamErrorCode,
pub params: Vec<(&'static str, String)>,
}
#[derive(Debug, Clone)]
pub struct DiagnosticBundle {
pub system: SystemSnapshot,
pub runtime: RuntimeSnapshot,
pub error: Option<ErrorContext>,
pub full: bool,
}
impl DiagnosticBundle {
pub fn capture() -> Self {
Self {
system: SystemSnapshot::capture(),
runtime: RuntimeSnapshot::default(),
error: None,
full: false,
}
}
pub fn capture_with_runtime(provider: &impl super::snapshot::HasRuntimeSnapshot) -> Self {
Self {
system: SystemSnapshot::capture(),
runtime: RuntimeSnapshot::capture(provider),
error: None,
full: false,
}
}
pub fn capture_with_error(code: NamErrorCode, params: Vec<(&'static str, String)>) -> Self {
Self {
system: SystemSnapshot::capture(),
runtime: RuntimeSnapshot::default(),
error: Some(ErrorContext { code, params }),
full: false,
}
}
pub fn capture_with_error_and_runtime(
code: NamErrorCode,
params: Vec<(&'static str, String)>,
provider: &impl super::snapshot::HasRuntimeSnapshot,
) -> Self {
Self {
system: SystemSnapshot::capture(),
runtime: RuntimeSnapshot::capture(provider),
error: Some(ErrorContext { code, params }),
full: false,
}
}
pub fn with_full(mut self, full: bool) -> Self {
self.full = full;
self
}
pub fn render(&self) -> String {
let separator = "────────────────────────────────────────────────";
let mut block = if let Some(ref err) = self.error {
format!(
"──── NAM-rs Diagnostic {separator}\n\
nam-rs v{} | {} | {}\n",
self.system.version,
err.code.code(),
err.code.mnemonic(),
)
} else {
format!(
"──── NAM-rs Diagnostic {separator}\n\
nam-rs v{}\n",
self.system.version,
)
};
if let Some(err) = self.error.as_ref().filter(|e| !e.params.is_empty()) {
let param_line: String = err
.params
.iter()
.map(|(k, v)| {
let v_redacted = {
let path = std::path::Path::new(v);
if path.is_absolute() {
redact_path(path, self.full)
} else {
v.clone()
}
};
format!("{k}={v_redacted}")
})
.collect::<Vec<_>>()
.join(" ");
block.push_str(¶m_line);
block.push('\n');
}
let model_name_raw = ACTIVE_MODEL_NAME
.read()
.unwrap_or_else(|e| e.into_inner())
.clone();
let model_name = if model_name_raw.is_empty() {
self.runtime
.model
.as_ref()
.map(|m| m.path_basename.clone())
.unwrap_or_default()
} else {
model_name_raw
};
let active_rate = {
let r = ACTIVE_SAMPLE_RATE.load(Ordering::Relaxed);
if r == 0 {
self.runtime.audio.sample_rate
} else {
r
}
};
block.push_str("──── Runtime State ─────────────────────────────\n");
let mut model_printed = false;
if !model_name.is_empty() {
let formatted = format_model_path(&model_name, self.full);
block.push_str(&format!("model={}\n", formatted));
model_printed = true;
}
if active_rate != 0 {
block.push_str(&format!("sample_rate={}\n", active_rate));
}
if let Some(ref model) = self.runtime.model {
let path_val = if self.full {
model.path_basename.clone()
} else {
let path = std::path::Path::new(&model.path_basename);
if let Some(file_name) = path.file_name() {
file_name.to_string_lossy().into_owned()
} else {
model.path_basename.clone()
}
};
block.push_str(&format!(
"model.arch={}\n\
model.topology={}\n\
model.channels={}\n\
model.receptive_field={}\n\
model.sample_rate={}\n\
model.weights_layout={}\n\
model.path_basename={}\n",
model.arch_label,
model.topology,
model.channels,
model.receptive_field,
model.model_sample_rate,
model.weights_layout,
path_val
));
model_printed = true;
}
if !model_printed {
block.push_str("model=none\n");
}
block.push_str(&format!(
"audio.sr={}\n\
audio.buffer_size={}\n\
audio.channel_count={}\n\
audio.host_name={}\n\
rt.prio={}\n\
rt.scheduler={}\n\
rt.cpu_pinned={}\n\
rt.huge_pages_active={}\n\
rt.huge_page_mode={}\n\
telemetry.p50_us={}\n\
telemetry.p99_us={}\n\
telemetry.p999_us={}\n\
telemetry.max_us={}\n\
telemetry.total_blocks={}\n\
telemetry.xruns={}\n\
telemetry.drains={}\n\
flags_seen={:#x}\n",
self.runtime.audio.sample_rate,
self.runtime.audio.buffer_size,
self.runtime.audio.channel_count,
self.runtime.audio.host_name,
self.runtime.rt.thread_priority,
self.runtime.rt.scheduler,
self.runtime
.rt
.cpu_pinned
.map(|c| c.to_string())
.unwrap_or_else(|| "none".to_string()),
self.runtime.rt.huge_pages_active,
self.runtime.rt.huge_page_mode,
self.runtime.telemetry.p50_us,
self.runtime.telemetry.p99_us,
self.runtime.telemetry.p999_us,
self.runtime.telemetry.max_us,
self.runtime.telemetry.total_blocks,
self.runtime.telemetry.xruns,
self.runtime.telemetry.drains,
self.runtime.flags_seen
));
block.push_str(&format!(
"arch={}\n\
os={} kernel={}\n\
pipewire={}\n\
features={}\n\
timestamp={}\n",
self.system.arch,
self.system.os,
self.system.kernel,
self.system.pipewire_version.as_deref().unwrap_or("N/A"),
if self.system.features.is_empty() {
"none (baseline x86-64-v3 only)".to_string()
} else {
self.system.features.join(", ")
},
timestamp(),
));
if let Some(buffer) = NamLogger::log_buffer() {
let trace = buffer.render_trace(50);
if !trace.is_empty() {
let trace_formatted = redact_text(&trace, self.full);
block.push_str("──── Recent Log Trace ─────────────────────────\n");
block.push_str(&trace_formatted);
}
}
block.push_str(&format!(
"{separator}\n\
Copy the block above when opening a support ticket.",
));
block
}
}