#![forbid(unsafe_code)]
use std::fmt;
use std::io::Write;
pub trait DiagnosticRecord: fmt::Debug + Clone {
fn to_jsonl(&self) -> String;
}
pub trait DiagnosticHookDispatch<E>: fmt::Debug {
fn dispatch(&self, entry: &E);
}
#[must_use]
pub fn json_string_literal(value: &str) -> String {
use std::fmt::Write as _;
let mut out = String::with_capacity(value.len() + 2);
out.push('"');
for ch in value.chars() {
match ch {
'"' => out.push_str("\\\""),
'\\' => out.push_str("\\\\"),
'\n' => out.push_str("\\n"),
'\r' => out.push_str("\\r"),
'\t' => out.push_str("\\t"),
'\u{08}' => out.push_str("\\b"),
'\u{0C}' => out.push_str("\\f"),
c if c < '\u{20}' => {
let _ = write!(&mut out, "\\u{:04x}", c as u32);
}
c => out.push(c),
}
}
out.push('"');
out
}
#[derive(Debug)]
pub struct DiagnosticLog<E: DiagnosticRecord> {
entries: Vec<E>,
head: usize,
max_entries: usize,
write_stderr: bool,
}
impl<E: DiagnosticRecord> Default for DiagnosticLog<E> {
fn default() -> Self {
Self::new()
}
}
impl<E: DiagnosticRecord> DiagnosticLog<E> {
pub fn new() -> Self {
Self {
entries: Vec::new(),
head: 0,
max_entries: 10_000,
write_stderr: false,
}
}
#[must_use]
pub fn with_stderr(mut self) -> Self {
self.write_stderr = true;
self
}
#[must_use]
pub fn with_max_entries(mut self, max: usize) -> Self {
self.max_entries = max;
self
}
pub fn record(&mut self, entry: E) {
if self.write_stderr {
let _ = writeln!(std::io::stderr(), "{}", entry.to_jsonl());
}
self.entries.push(entry);
if self.max_entries > 0 && self.entries.len().saturating_sub(self.head) > self.max_entries {
self.head += 1;
if self.head >= self.entries.len() / 2 {
self.entries = self.entries.split_off(self.head);
self.head = 0;
}
}
}
pub fn entries(&self) -> &[E] {
&self.entries[self.head..]
}
pub fn entries_matching(&self, predicate: impl Fn(&E) -> bool) -> Vec<&E> {
self.entries().iter().filter(|e| predicate(e)).collect()
}
pub fn clear(&mut self) {
self.entries.clear();
self.head = 0;
}
pub fn to_jsonl(&self) -> String {
self.entries()
.iter()
.map(DiagnosticRecord::to_jsonl)
.collect::<Vec<_>>()
.join("\n")
}
pub fn len(&self) -> usize {
self.entries.len().saturating_sub(self.head)
}
pub fn is_empty(&self) -> bool {
self.len() == 0
}
}
pub struct DiagnosticSupport<E: DiagnosticRecord, H: DiagnosticHookDispatch<E>> {
log: Option<DiagnosticLog<E>>,
hooks: Option<H>,
}
impl<E: DiagnosticRecord, H: DiagnosticHookDispatch<E>> Default for DiagnosticSupport<E, H> {
fn default() -> Self {
Self::new()
}
}
impl<E: DiagnosticRecord, H: DiagnosticHookDispatch<E>> fmt::Debug for DiagnosticSupport<E, H> {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.debug_struct("DiagnosticSupport")
.field("log", &self.log)
.field("hooks", &self.hooks)
.finish()
}
}
impl<E: DiagnosticRecord, H: DiagnosticHookDispatch<E>> DiagnosticSupport<E, H> {
pub fn new() -> Self {
Self {
log: None,
hooks: None,
}
}
#[must_use]
pub fn with_log(mut self, log: DiagnosticLog<E>) -> Self {
self.log = Some(log);
self
}
#[must_use]
pub fn with_hooks(mut self, hooks: H) -> Self {
self.hooks = Some(hooks);
self
}
pub fn set_log(&mut self, log: DiagnosticLog<E>) {
self.log = Some(log);
}
pub fn set_hooks(&mut self, hooks: H) {
self.hooks = Some(hooks);
}
pub fn log(&self) -> Option<&DiagnosticLog<E>> {
self.log.as_ref()
}
pub fn log_mut(&mut self) -> Option<&mut DiagnosticLog<E>> {
self.log.as_mut()
}
pub fn hooks(&self) -> Option<&H> {
self.hooks.as_ref()
}
pub fn is_active(&self) -> bool {
self.log.is_some() || self.hooks.is_some()
}
pub fn record(&mut self, entry: E) {
if let Some(ref hooks) = self.hooks {
hooks.dispatch(&entry);
}
if let Some(ref mut log) = self.log {
log.record(entry);
}
}
}
pub type TelemetryCallback<E> = Box<dyn Fn(&E) + Send + Sync>;
pub fn fnv1a_hash(data: &[u8]) -> u64 {
let mut hash: u64 = 0xcbf29ce484222325;
for &b in data {
hash ^= b as u64;
hash = hash.wrapping_mul(0x100000001b3);
}
hash
}
pub fn env_flag_enabled(var_name: &str) -> bool {
std::env::var(var_name)
.map(|v| env_flag_value_enabled(&v))
.unwrap_or(false)
}
fn env_flag_value_enabled(value: &str) -> bool {
value == "1" || value.eq_ignore_ascii_case("true")
}
#[cfg(test)]
mod tests {
use super::*;
#[derive(Debug, Clone)]
struct TestEntry {
kind: &'static str,
value: u64,
}
impl DiagnosticRecord for TestEntry {
fn to_jsonl(&self) -> String {
format!("{{\"kind\":\"{}\",\"value\":{}}}", self.kind, self.value)
}
}
#[test]
fn log_records_and_retrieves() {
let mut log = DiagnosticLog::<TestEntry>::new();
log.record(TestEntry {
kind: "a",
value: 1,
});
log.record(TestEntry {
kind: "b",
value: 2,
});
assert_eq!(log.len(), 2);
assert_eq!(log.entries()[0].value, 1);
assert_eq!(log.entries()[1].value, 2);
}
#[test]
fn log_evicts_oldest_when_full() {
let mut log = DiagnosticLog::<TestEntry>::new().with_max_entries(2);
log.record(TestEntry {
kind: "a",
value: 1,
});
log.record(TestEntry {
kind: "b",
value: 2,
});
log.record(TestEntry {
kind: "c",
value: 3,
});
assert_eq!(log.len(), 2);
assert_eq!(log.entries()[0].value, 2);
assert_eq!(log.entries()[1].value, 3);
}
#[test]
fn log_preserves_order_after_many_evictions() {
let mut log = DiagnosticLog::<TestEntry>::new().with_max_entries(3);
for value in 0..16 {
log.record(TestEntry { kind: "x", value });
}
let values: Vec<u64> = log.entries().iter().map(|entry| entry.value).collect();
assert_eq!(values, vec![13, 14, 15]);
}
#[test]
fn log_clear() {
let mut log = DiagnosticLog::<TestEntry>::new();
log.record(TestEntry {
kind: "a",
value: 1,
});
assert!(!log.is_empty());
log.clear();
assert!(log.is_empty());
assert_eq!(log.len(), 0);
}
#[test]
fn log_to_jsonl() {
let mut log = DiagnosticLog::<TestEntry>::new();
log.record(TestEntry {
kind: "x",
value: 10,
});
log.record(TestEntry {
kind: "y",
value: 20,
});
let output = log.to_jsonl();
assert!(output.contains("\"kind\":\"x\""));
assert!(output.contains("\"kind\":\"y\""));
assert!(output.contains('\n'));
}
#[test]
fn log_entries_matching() {
let mut log = DiagnosticLog::<TestEntry>::new();
log.record(TestEntry {
kind: "a",
value: 1,
});
log.record(TestEntry {
kind: "b",
value: 2,
});
log.record(TestEntry {
kind: "a",
value: 3,
});
let matches = log.entries_matching(|e| e.kind == "a");
assert_eq!(matches.len(), 2);
}
#[test]
fn json_string_literal_escapes_control_characters() {
let escaped = json_string_literal("line 1\nline\t2");
assert_eq!(escaped, "\"line 1\\nline\\t2\"");
}
#[test]
fn fnv1a_hash_deterministic() {
let h1 = fnv1a_hash(b"hello world");
let h2 = fnv1a_hash(b"hello world");
assert_eq!(h1, h2);
assert_ne!(h1, fnv1a_hash(b"hello worlD"));
}
#[test]
fn fnv1a_hash_empty() {
let h = fnv1a_hash(b"");
assert_eq!(h, 0xcbf29ce484222325); }
#[test]
fn env_flag_enabled_false_when_unset() {
assert!(!env_flag_enabled("FTUI_TEST_DIAGNOSTICS_NEVER_SET_12345"));
}
#[test]
fn env_flag_enabled_accepts_true_case_insensitively() {
assert!(env_flag_value_enabled("TrUe"));
}
#[test]
fn env_flag_enabled_accepts_one() {
assert!(env_flag_value_enabled("1"));
}
#[test]
fn default_log_has_correct_capacity() {
let log = DiagnosticLog::<TestEntry>::new();
assert_eq!(log.max_entries, 10_000);
assert!(!log.write_stderr);
}
}