use core::error::request_value;
use core::fmt;
use std::borrow::Cow;
use std::hash::{Hash, Hasher};
use crate::ErrorCategory;
use crate::errors::{ErrorCode, lookup_error};
use crate::exn::{Attachment, BuiltinKey, Fault, Frame, FrameKind, Placement, frame_category};
pub const REPORT_FORMAT: &str = "fast-observe/1";
#[cfg(feature = "serde")]
pub const REPORT_SCHEMA_VERSION: u32 = 2;
const APPENDIX_MAX_LINES: usize = 32;
const SOURCE_LINE_MAX_CHARS: usize = 200;
static REPORT_SOURCE: std::sync::LazyLock<bool> = std::sync::LazyLock::new(|| {
crate::config::env_enum(
crate::env_vars::OBSERVE_REPORT_SOURCE,
|name| match name {
"1" | "true" => Some(true),
"0" | "false" | "" => Some(false),
_ => None,
},
false,
"1|true|0|false",
)
});
fn source_line_at(location: &std::panic::Location) -> Option<String> {
if !*REPORT_SOURCE {
return None;
}
let file = location.file();
let contents =
crate::diagnostic::registered_source(file).or_else(|| fs_err::read_to_string(file).ok())?;
let line = contents
.lines()
.nth(location.line() as usize - 1)?
.trim_end();
let capped: String = line.chars().take(SOURCE_LINE_MAX_CHARS).collect();
Some(capped)
}
struct Line<'a>(Cow<'a, str>);
impl<'a> Line<'a> {
fn new(s: &'a str) -> Self {
if s.contains(['\n', '\r']) {
Self(Cow::Owned(s.replace('\n', "\\n").replace('\r', "\\r")))
} else {
Self(Cow::Borrowed(s))
}
}
}
impl fmt::Display for Line<'_> {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.write_str(&self.0)
}
}
fn concrete_type_name(type_name: &'static str) -> Option<&'static str> {
(!type_name.starts_with("dyn ")).then_some(type_name)
}
struct CauseRow {
code: Option<ErrorCode>,
type_name: Option<&'static str>,
message: String,
location: &'static std::panic::Location<'static>,
kind: Option<FrameKind>,
}
struct ReportData {
code: Option<ErrorCode>,
category: Option<ErrorCategory>,
type_name: Option<&'static str>,
message: String,
location: &'static std::panic::Location<'static>,
source_line: Option<String>,
scope: Option<(String, Option<String>)>,
scope_elapsed_ms: Option<u128>,
attachments: Vec<(Option<&'static str>, String)>,
causes: Vec<CauseRow>,
trace_id: Option<String>,
advice: Option<&'static str>,
action: Option<String>,
hint: Option<String>,
fingerprint: String,
appendix: Vec<(&'static str, String)>,
}
fn frame_code(frame: &Frame) -> Option<ErrorCode> {
request_value::<ErrorCode>(frame.error())
}
fn find_keyed(frame: &Frame, key: BuiltinKey) -> Option<&Attachment> {
frame
.attachments()
.iter()
.find(|a| matches!(a.key(), Some(k) if k == key.as_str()))
}
fn is_reserved_key(key: Option<&'static str>) -> bool {
[
BuiltinKey::ScopePath,
BuiltinKey::ScopeElapsedMs,
BuiltinKey::TraceId,
]
.into_iter()
.any(|k| Some(k.as_str()) == key)
}
fn advice_for(code: Option<ErrorCode>) -> Option<&'static str> {
code.and_then(|c| lookup_error(c.0))
.and_then(|entry| entry.advice)
}
fn action_for(code: Option<ErrorCode>) -> Option<&'static str> {
code.and_then(|c| lookup_error(c.0))
.and_then(|entry| entry.action)
}
fn base_action(category: ErrorCategory, code: Option<ErrorCode>) -> &'static str {
action_for(code).unwrap_or_else(|| category.policy().advice_line())
}
#[allow(
clippy::cast_possible_truncation,
reason = "deliberate: the fingerprint is the low 32 bits of the hash"
)]
fn fingerprint_of(root: &Frame) -> String {
let mut hasher = std::collections::hash_map::DefaultHasher::new();
match frame_code(root) {
Some(code) => code.0.hash(&mut hasher),
None => root.type_name().hash(&mut hasher),
}
let loc = root.location();
(loc.file(), loc.line(), loc.column()).hash(&mut hasher);
let mut cause = root;
while let Some((_, child)) = cause.child_edges().next() {
cause = child;
}
cause.type_name().hash(&mut hasher);
let hash = hasher.finish();
format!("{:08x}", hash as u32)
}
impl ReportData {
fn collect(root: &Frame) -> Self {
let code = frame_code(root);
let category = frame_category(root);
let scope = find_keyed(root, BuiltinKey::ScopePath).map(|path| {
(
path.display().to_string(),
find_keyed(root, BuiltinKey::ScopeElapsedMs).map(|ms| ms.display().to_string()),
)
});
let scope_elapsed_ms = find_keyed(root, BuiltinKey::ScopeElapsedMs)
.and_then(Attachment::downcast::<u128>)
.copied();
let attachments = root
.attachments()
.iter()
.filter(|a| a.placement() == Placement::Inline && !is_reserved_key(a.key()))
.map(|a| (a.key(), a.display().to_string()))
.collect();
let mut causes = Vec::new();
let mut stack: Vec<(Option<FrameKind>, &Frame)> = vec![(None, root)];
while let Some((kind, frame)) = stack.pop() {
causes.push(CauseRow {
code: frame_code(frame),
type_name: concrete_type_name(frame.type_name()),
message: frame.to_string(),
location: frame.location(),
kind,
});
for (kind, child) in frame.child_edges().rev() {
stack.push((Some(kind), child));
}
}
let appendix = root
.attachments()
.iter()
.filter(|a| a.placement() == Placement::Appendix)
.filter_map(|a| a.key().map(|k| (k, a.display().to_string())))
.collect();
let action = category.map(|c| base_action(c, code).to_string());
let hint = code.map(|c| format!("run `doctor {}`", c.0));
Self {
code,
category,
type_name: concrete_type_name(root.type_name()),
message: root.to_string(),
location: root.location(),
source_line: source_line_at(root.location()),
scope,
scope_elapsed_ms,
attachments,
causes,
trace_id: find_keyed(root, BuiltinKey::TraceId).map(|t| t.display().to_string()),
advice: advice_for(code),
action,
hint,
fingerprint: fingerprint_of(root),
appendix,
}
}
}
fn cause_label(kind: Option<FrameKind>, n: usize) -> String {
let word = match kind {
None | Some(FrameKind::Source) => "cause",
Some(FrameKind::Wrap) => "original",
Some(FrameKind::Attempt) => "attempt",
Some(FrameKind::Batch) => "failure",
};
format!("{word} {n}")
}
fn write_cause_line(w: &mut impl fmt::Write, n: usize, cause: &CauseRow) -> fmt::Result {
write!(w, "{}: ", cause_label(cause.kind, n))?;
if let Some(code) = cause.code {
write!(w, "[{}] ", code.0)?;
}
if let Some(type_name) = cause.type_name {
write!(w, "[{type_name}] ")?;
}
write!(w, "{}", Line::new(&cause.message))?;
let loc = cause.location;
writeln!(w, ", at {}:{}:{}", loc.file(), loc.line(), loc.column())
}
fn write_report(w: &mut impl fmt::Write, root: &Frame) -> fmt::Result {
let data = ReportData::collect(root);
writeln!(w, "report: {REPORT_FORMAT}")?;
w.write_str("error: ")?;
if let Some(code) = data.code {
write!(w, "[{}] ", code.0)?;
}
if let Some(type_name) = data.type_name {
write!(w, "[{type_name}] ")?;
}
writeln!(w, "{}", Line::new(&data.message))?;
if let Some(category) = data.category {
writeln!(
w,
"category: {category} (policy: {})",
category.policy().advice_line()
)?;
}
let loc = data.location;
writeln!(
w,
"location: {}:{}:{}",
loc.file(),
loc.line(),
loc.column()
)?;
if let Some(source) = &data.source_line {
writeln!(w, "source: {}", Line::new(source))?;
}
if let Some((path, elapsed)) = &data.scope {
write!(w, "scope: {}", Line::new(path))?;
match data.scope_elapsed_ms {
Some(ms) => write!(w, " (elapsed {ms}ms)")?,
None => {
if let Some(ms) = elapsed {
write!(w, " (elapsed {}ms)", Line::new(ms))?;
}
}
}
w.write_char('\n')?;
}
for (key, display) in &data.attachments {
match key {
Some(key) => writeln!(w, "attachment: {}={}", Line::new(key), Line::new(display))?,
None => writeln!(w, "attachment: {}", Line::new(display))?,
}
}
for (n, cause) in data.causes.iter().enumerate() {
write_cause_line(w, n, cause)?;
}
if let Some(trace) = &data.trace_id {
writeln!(w, "trace_id: {}", Line::new(trace))?;
}
writeln!(w, "fingerprint: {}", data.fingerprint)?;
if let Some(advice) = data.advice {
writeln!(w, "advice: {advice}")?;
}
if let Some(action) = &data.action {
writeln!(w, "action: {}", Line::new(action))?;
}
if let Some(hint) = &data.hint {
writeln!(w, "hint: {hint}")?;
}
for (key, display) in &data.appendix {
let mut lines = display.trim_end().lines();
let mut shown = 0usize;
let mut first = true;
for line in lines.by_ref().take(APPENDIX_MAX_LINES) {
if first {
writeln!(w, "appendix {key}: {}", Line::new(line))?;
first = false;
} else {
writeln!(w, " {}", Line::new(line))?;
}
shown += 1;
}
let remaining = lines.count();
if remaining > 0 {
writeln!(w, " … ({remaining} more lines)")?;
}
let _ = shown;
}
Ok(())
}
#[must_use]
pub fn render_report<E: Send + Sync + 'static>(fault: &Fault<E>) -> String {
report_display(fault).to_string()
}
#[must_use]
pub fn render_frame_report(root: &Frame) -> String {
let mut out = String::new();
let _ = write_report(&mut out, root);
out
}
#[must_use]
pub fn report_display<E: Send + Sync + 'static>(fault: &Fault<E>) -> impl fmt::Display + '_ {
ReportDisplay { fault }
}
struct ReportDisplay<'a, E: Send + Sync + 'static> {
fault: &'a Fault<E>,
}
impl<E: Send + Sync + 'static> fmt::Display for ReportDisplay<'_, E> {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write_report(f, self.fault.frame())
}
}
#[cfg(feature = "serde")]
fn push_json_escaped(out: &mut String, value: &str) {
use fmt::Write as _;
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"),
c if u32::from(c) < 0x20 => {
let _ = write!(out, "\\u{:04x}", u32::from(c));
}
c => out.push(c),
}
}
out.push('"');
}
#[cfg(feature = "serde")]
fn push_json_field(out: &mut String, first: &mut bool, key: &str, value: &str) {
if !*first {
out.push_str(", ");
}
*first = false;
push_json_escaped(out, key);
out.push_str(": ");
push_json_escaped(out, value);
}
#[cfg(feature = "serde")]
fn push_json_number(out: &mut String, first: &mut bool, key: &str, value: impl fmt::Display) {
use fmt::Write as _;
if !*first {
out.push_str(", ");
}
*first = false;
push_json_escaped(out, key);
out.push_str(": ");
let _ = write!(out, "{value}");
}
#[cfg(feature = "serde")]
fn push_json_location(out: &mut String, first: &mut bool, loc: &std::panic::Location) {
out.push_str(if *first { "" } else { ", " });
*first = false;
out.push_str("\"location\": {");
let mut inner = true;
push_json_field(out, &mut inner, "file", loc.file());
push_json_number(out, &mut inner, "line", loc.line());
push_json_number(out, &mut inner, "column", loc.column());
out.push('}');
}
#[cfg(feature = "serde")]
#[must_use]
pub fn render_report_json<E: Send + Sync + 'static>(fault: &Fault<E>) -> String {
render_frame_report_json(fault.frame())
}
#[cfg(feature = "serde")]
#[must_use]
pub fn render_frame_report_json(root: &Frame) -> String {
let data = ReportData::collect(root);
let mut out = String::from("{");
let mut first = true;
push_json_number(&mut out, &mut first, "schema", REPORT_SCHEMA_VERSION);
push_json_field(&mut out, &mut first, "report", REPORT_FORMAT);
out.push_str(", \"error\": {");
let mut inner = true;
if let Some(code) = data.code {
push_json_field(&mut out, &mut inner, "code", code.0);
}
if let Some(type_name) = data.type_name {
push_json_field(&mut out, &mut inner, "type", type_name);
}
push_json_field(&mut out, &mut inner, "message", &data.message);
if let Some(category) = data.category {
push_json_field(&mut out, &mut inner, "category", category.as_ref());
push_json_field(
&mut out,
&mut inner,
"policy",
category.policy().advice_line(),
);
}
if let Some(advice) = data.advice {
push_json_field(&mut out, &mut inner, "advice", advice);
}
out.push('}');
push_json_location(&mut out, &mut first, data.location);
if let Some(source) = &data.source_line {
push_json_field(&mut out, &mut first, "source", source);
}
if let Some((path, _)) = &data.scope {
out.push_str(", \"scope\": {");
let mut inner = true;
push_json_field(&mut out, &mut inner, "path", path);
if let Some(ms) = data.scope_elapsed_ms {
push_json_number(&mut out, &mut inner, "elapsed_ms", ms);
}
out.push('}');
}
if !data.attachments.is_empty() {
out.push_str(", \"attachments\": [");
for (i, (key, display)) in data.attachments.iter().enumerate() {
if i > 0 {
out.push_str(", ");
}
out.push('{');
let mut inner = true;
if let Some(key) = key {
push_json_field(&mut out, &mut inner, "key", key);
}
push_json_field(&mut out, &mut inner, "value", display);
out.push('}');
}
out.push(']');
}
out.push_str(", \"causes\": [");
for (i, cause) in data.causes.iter().enumerate() {
if i > 0 {
out.push_str(", ");
}
out.push('{');
let mut inner = true;
if let Some(code) = cause.code {
push_json_field(&mut out, &mut inner, "code", code.0);
}
if let Some(type_name) = cause.type_name {
push_json_field(&mut out, &mut inner, "type", type_name);
}
push_json_field(&mut out, &mut inner, "message", &cause.message);
if let Some(kind) = cause.kind {
push_json_field(&mut out, &mut inner, "kind", kind.as_ref());
}
push_json_location(&mut out, &mut inner, cause.location);
out.push('}');
}
out.push(']');
if let Some(trace) = &data.trace_id {
push_json_field(&mut out, &mut first, "trace_id", trace);
}
push_json_field(&mut out, &mut first, "fingerprint", &data.fingerprint);
if let Some(action) = &data.action {
push_json_field(&mut out, &mut first, "action", action);
}
if let Some(hint) = &data.hint {
push_json_field(&mut out, &mut first, "hint", hint);
}
if !data.appendix.is_empty() {
out.push_str(", \"appendix\": {");
let mut inner = true;
for (key, display) in &data.appendix {
push_json_field(&mut out, &mut inner, key, display);
}
out.push('}');
}
out.push('}');
out
}