use crate::{CaptureSite, DiagnosticCategory, DiagnosticCode, DiagnosticStage};
use serde::{Serialize, Serializer};
#[derive(Clone, Copy)]
pub struct InlineDiagnosticText {
bytes: [u8; 192],
len: u8,
truncated: bool,
redacted: bool,
}
impl InlineDiagnosticText {
pub fn metadata(value: &str) -> Self {
let mut out = Self {
bytes: [0; 192],
len: 0,
truncated: false,
redacted: false,
};
let mut len = value.len().min(192);
while !value.is_char_boundary(len) {
len -= 1;
}
out.truncated = len < value.len();
if !value[..len]
.chars()
.all(|c| c.is_alphanumeric() || "_:<>[],(); &*.-".contains(c))
{
out.redacted = true;
return out;
}
out.bytes[..len].copy_from_slice(&value.as_bytes()[..len]);
out.len = len as u8;
out.truncated = len < value.len();
out
}
}
impl Serialize for InlineDiagnosticText {
fn serialize<S: Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
use serde::ser::SerializeStruct;
let mut value = serializer.serialize_struct("InlineDiagnosticText", 3)?;
value.serialize_field(
"value",
std::str::from_utf8(&self.bytes[..self.len as usize]).unwrap_or(""),
)?;
value.serialize_field("truncated", &self.truncated)?;
value.serialize_field("redacted", &self.redacted)?;
value.end()
}
}
#[derive(Clone, Copy, Serialize)]
#[serde(rename_all = "snake_case")]
pub enum DiagnosticFactUnavailable {
OpaqueSource,
MetadataUnavailable,
NotApplicable,
}
#[derive(Clone, Copy, Serialize)]
pub struct RegisteredDiagnosticCode(&'static str);
impl RegisteredDiagnosticCode {
pub fn from_registered(code: &'static str, registry: &[&'static str]) -> Option<Self> {
(!code.is_empty()
&& code.len() <= 128
&& code
.bytes()
.all(|b| b.is_ascii_alphanumeric() || b"._-".contains(&b))
&& registry.len() <= 256
&& registry.contains(&code))
.then_some(Self(code))
}
}
#[derive(Clone, Copy, Serialize)]
pub struct BoundedDiagnosticCause {
stage: DiagnosticStage,
code: DiagnosticCode,
io_kind: Option<DiagnosticCode>,
os_code: Option<i32>,
database_code: Option<u32>,
sqlstate: Option<InlineDiagnosticText>,
column_index: Option<u64>,
column_count: Option<u64>,
target_rust_type: Option<InlineDiagnosticText>,
actual_db_type: Option<InlineDiagnosticText>,
object: Option<InlineDiagnosticText>,
unavailable: Option<DiagnosticFactUnavailable>,
target_type_unavailable: Option<DiagnosticFactUnavailable>,
actual_type_unavailable: Option<DiagnosticFactUnavailable>,
}
impl BoundedDiagnosticCause {
pub fn new(stage: DiagnosticStage, code: DiagnosticCode) -> Self {
Self {
stage,
code,
io_kind: None,
os_code: None,
database_code: None,
sqlstate: None,
column_index: None,
column_count: None,
target_rust_type: None,
actual_db_type: None,
object: None,
unavailable: None,
target_type_unavailable: Some(DiagnosticFactUnavailable::MetadataUnavailable),
actual_type_unavailable: Some(DiagnosticFactUnavailable::MetadataUnavailable),
}
}
pub fn with_system(mut self, kind: DiagnosticCode, os: Option<i32>) -> Self {
self.io_kind = Some(kind);
self.os_code = os;
self
}
pub fn with_database_code(mut self, code: u32) -> Self {
self.database_code = Some(code);
self
}
pub fn with_sqlstate(mut self, state: &str) -> Self {
self.sqlstate = (state.len() == 5
&& state
.bytes()
.all(|b| b.is_ascii_uppercase() || b.is_ascii_digit()))
.then(|| InlineDiagnosticText::metadata(state));
self
}
pub fn with_column(mut self, index: Option<u64>, count: Option<u64>) -> Self {
self.column_index = index;
self.column_count = count;
self
}
pub fn with_types(
mut self,
target: Option<InlineDiagnosticText>,
actual: Option<InlineDiagnosticText>,
unavailable: Option<DiagnosticFactUnavailable>,
) -> Self {
self.target_rust_type = target;
self.actual_db_type = actual;
self.unavailable = unavailable;
self.target_type_unavailable = if target.is_some() {
None
} else {
Some(unavailable.unwrap_or(DiagnosticFactUnavailable::MetadataUnavailable))
};
self.actual_type_unavailable = if actual.is_some() {
None
} else {
Some(unavailable.unwrap_or(DiagnosticFactUnavailable::MetadataUnavailable))
};
self
}
pub fn with_object(mut self, object: InlineDiagnosticText) -> Self {
self.object = Some(object);
self
}
}
#[derive(Serialize)]
pub struct BoundedDiagnostic {
diagnostic_id: u64,
primary_diagnostic_id: Option<u64>,
category: DiagnosticCategory,
capture_site: CaptureSite,
origin_file: &'static str,
origin_line: u32,
origin_column: u32,
causes: [Option<BoundedDiagnosticCause>; 4],
cause_count: usize,
omitted_causes: u64,
stack_status: &'static str,
}
impl BoundedDiagnostic {
#[track_caller]
pub fn capture(
category: DiagnosticCategory,
site: CaptureSite,
cause: BoundedDiagnosticCause,
) -> Self {
let origin = std::panic::Location::caller();
let file = origin
.file()
.rsplit("/crates/")
.next()
.unwrap_or(origin.file());
let file = if file.starts_with('/') || file.contains('\\') {
file.rsplit(['/', '\\']).next().unwrap_or("unknown")
} else {
file
};
Self {
diagnostic_id: crate::diagnostic::NEXT_ID
.fetch_add(1, std::sync::atomic::Ordering::Relaxed),
primary_diagnostic_id: None,
category,
capture_site: site,
origin_file: file,
origin_line: origin.line(),
origin_column: origin.column(),
causes: [Some(cause), None, None, None],
cause_count: 1,
omitted_causes: 0,
stack_status: "unavailable_bounded_capture",
}
}
pub fn wrap(mut self, cause: BoundedDiagnosticCause) -> Self {
if self.cause_count == self.causes.len() {
self.omitted_causes = self.omitted_causes.saturating_add(1);
} else {
self.causes.copy_within(0..self.cause_count, 1);
self.causes[0] = Some(cause);
self.cause_count += 1;
}
self
}
pub fn during_cleanup_of(mut self, primary: &Self) -> Self {
self.primary_diagnostic_id = Some(primary.diagnostic_id);
self
}
pub fn id(&self) -> u64 {
self.diagnostic_id
}
pub fn category(&self) -> DiagnosticCategory {
self.category
}
pub fn during_cleanup_occurrence(mut self, primary: DiagnosticOccurrence) -> Self {
self.primary_diagnostic_id = Some(primary.source_id());
self
}
pub fn occurrence(&self) -> DiagnosticOccurrence {
DiagnosticOccurrence {
diagnostic_id: self.diagnostic_id,
primary_diagnostic_id: self.primary_diagnostic_id,
}
}
}
#[derive(Clone, Copy, Serialize)]
pub struct DiagnosticOccurrence {
diagnostic_id: u64,
primary_diagnostic_id: Option<u64>,
}
impl DiagnosticOccurrence {
pub(crate) const fn source_id(&self) -> u64 {
self.diagnostic_id
}
pub(crate) fn from_diagnostic(diagnostic: &crate::Diagnostic) -> Self {
Self {
diagnostic_id: diagnostic.id(),
primary_diagnostic_id: diagnostic.primary_id_for_projection(),
}
}
}
#[derive(Clone, Copy, Serialize)]
#[serde(rename_all = "snake_case")]
pub enum OperationOutcome {
Succeeded,
Rejected,
Failed,
Cancelled,
TimedOut,
Panicked,
Unknown,
}
#[derive(Clone, Copy, Serialize)]
#[serde(rename_all = "snake_case")]
pub enum PhysicalDispositionFact {
NotUsed,
Returned,
Discarded,
Unknown,
}
#[derive(Clone, Copy, Serialize)]
#[serde(rename_all = "snake_case")]
pub enum BusinessOutcome {
Success,
Failure,
Unknown,
}
#[derive(Clone, Copy, Serialize)]
#[serde(rename_all = "snake_case")]
pub enum ResponseDelivery {
NotStarted,
Partial,
LocalWriteComplete,
Failed,
Cancelled,
TimedOut,
Unknown,
}
#[derive(Clone, Copy, Serialize)]
#[serde(rename_all = "snake_case")]
pub enum CleanupOutcome {
NotRun,
Succeeded,
Failed,
Unknown,
}
#[derive(Clone, Copy, Serialize)]
pub struct DiagnosticOutcomeAxes {
pub operation: OperationOutcome,
pub physical: PhysicalDispositionFact,
pub business: BusinessOutcome,
pub business_code: Option<RegisteredDiagnosticCode>,
pub delivery: ResponseDelivery,
pub bytes_written: Option<u64>,
pub cleanup: CleanupOutcome,
}
impl Default for DiagnosticOutcomeAxes {
fn default() -> Self {
Self {
operation: OperationOutcome::Unknown,
physical: PhysicalDispositionFact::Unknown,
business: BusinessOutcome::Unknown,
business_code: None,
delivery: ResponseDelivery::Unknown,
bytes_written: None,
cleanup: CleanupOutcome::Unknown,
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn bounded_causes_primary_cleanup_metadata_and_business_code() {
let cause = BoundedDiagnosticCause::new(
DiagnosticStage::RequestDb,
DiagnosticCode::new("db.decode").unwrap(),
)
.with_database_code(1064)
.with_sqlstate("42000")
.with_types(
None,
Some(InlineDiagnosticText::metadata("VARCHAR")),
Some(DiagnosticFactUnavailable::OpaqueSource),
);
let mut primary = BoundedDiagnostic::capture(
DiagnosticCategory::UnexpectedError,
CaptureSite::FirstObserved,
cause,
);
let id = primary.id();
for _ in 0..6 {
primary = primary.wrap(cause);
}
let p = serde_json::to_value(&primary).unwrap();
assert_eq!(p["diagnostic_id"], id);
assert_eq!(p["omitted_causes"], 3);
assert_eq!(p["causes"][0]["sqlstate"]["value"], "42000");
assert_eq!(p["causes"][0]["target_type_unavailable"], "opaque_source");
assert!(p["causes"][0]["actual_type_unavailable"].is_null());
let cleanup = BoundedDiagnostic::capture(
DiagnosticCategory::UnexpectedError,
CaptureSite::FirstObserved,
cause,
)
.during_cleanup_of(&primary);
assert_ne!(cleanup.id(), id);
assert_eq!(
serde_json::to_value(cleanup).unwrap()["primary_diagnostic_id"],
id
);
let secret =
serde_json::to_value(InlineDiagnosticText::metadata("mysql://SECRET@host")).unwrap();
assert_eq!(secret["redacted"], true);
assert!(!secret.to_string().contains("SECRET"));
let long = serde_json::to_value(InlineDiagnosticText::metadata(&"界".repeat(100))).unwrap();
assert_eq!(long["truncated"], true);
assert!(
RegisteredDiagnosticCode::from_registered("INVALID_VALUE", &["INVALID_VALUE"])
.is_some()
);
assert!(RegisteredDiagnosticCode::from_registered("FOREIGN", &["INVALID_VALUE"]).is_none());
assert!(
RegisteredDiagnosticCode::from_registered("SECRET@host", &["SECRET@host"]).is_none()
);
}
}