use prost::Message;
use tonic::{Code, Status};
use crate::{Aerro, Category, Exposure, Frame, ServiceFailure};
use super::envelope::{ENVELOPE_VERSION, to_proto};
use super::raw;
#[derive(Debug, Copy, Clone)]
pub struct EncodeOptions {
pub exposure: Exposure,
pub max_frames: u8,
}
impl Default for EncodeOptions {
fn default() -> Self {
Self {
exposure: Exposure::Internal,
max_frames: 16,
}
}
}
pub fn encode<E: Aerro>(sf: &ServiceFailure<E>, opts: &EncodeOptions) -> Status {
let route = opts.exposure;
let outer_code = sf.inner.code();
let outer_msg = redact_message(&sf.inner, route);
let mut payload = Vec::new();
if let Err(e) = sf.inner.encode_payload(route, &mut payload) {
return Status::new(Code::Internal, format!("aerro: encode failed: {e}"));
}
let wire_frames = if route == Exposure::Public {
Vec::new()
} else {
elide_to_cap(&sf.frames, opts.max_frames)
};
let env = raw::Envelope {
category: to_proto(sf.inner.category()) as i32,
type_id: Aerro::type_id(&sf.inner).to_string(),
trace_id: sf.trace.trace_id.to_vec(),
span_id: sf.trace.span_id.to_vec(),
frames: wire_frames,
payload,
version: ENVELOPE_VERSION,
};
let bytes = env.encode_to_vec();
Status::with_details(outer_code, outer_msg, bytes.into())
}
fn redact_message<E: Aerro>(inner: &E, route: Exposure) -> String {
if inner.category() == Category::System && route != Exposure::Internal {
"internal error".to_string()
} else {
inner.to_string()
}
}
fn elide_to_cap(frames: &[Frame], cap: u8) -> Vec<raw::Frame> {
let cap = cap.max(1) as usize;
if frames.len() <= cap {
return frames.iter().map(to_wire_frame).collect();
}
let keep_front = cap / 2;
let keep_back = cap.saturating_sub(keep_front + 1);
let n_elided = frames.len() - keep_front - keep_back;
let mut out = Vec::with_capacity(cap);
out.extend(frames[..keep_front].iter().map(to_wire_frame));
out.push(raw::Frame {
service: "...".into(),
rpc: "elided".into(),
code: 0,
message: format!("{} frames elided", n_elided),
location: String::new(),
category: to_proto(frames[keep_front].category) as i32,
});
if keep_back > 0 {
out.extend(frames[frames.len() - keep_back..].iter().map(to_wire_frame));
}
out
}
fn to_wire_frame(f: &Frame) -> raw::Frame {
raw::Frame {
service: f.service.to_string(),
rpc: f.rpc.to_string(),
code: f.code as i32 as u32,
message: f.message.to_string(),
location: f
.location
.map(|l| format!("{}:{}", l.file(), l.line()))
.unwrap_or_default(),
category: to_proto(f.category) as i32,
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::test_support::Boom;
use tonic::Code;
#[test]
fn system_message_redacted_at_public() {
let sf: ServiceFailure<Boom> = Boom { x: 9 }.into();
let st = encode(
&sf,
&EncodeOptions {
exposure: Exposure::Public,
max_frames: 16,
},
);
assert_eq!(st.code(), Code::Internal);
assert_eq!(st.message(), "internal error");
}
#[test]
fn system_message_kept_at_internal() {
let sf: ServiceFailure<Boom> = Boom { x: 9 }.into();
let st = encode(&sf, &EncodeOptions::default());
assert_eq!(st.code(), Code::Internal);
assert!(st.message().starts_with("toy.boom"));
}
#[test]
fn elision_keeps_cap() {
let mut sf: ServiceFailure<Boom> = Boom { x: 0 }.into();
for i in 0..20u32 {
sf.frames.push(Frame::local(
"svc",
"rpc",
Code::Internal,
format!("f{i}"),
Category::System,
));
}
let st = encode(
&sf,
&EncodeOptions {
exposure: Exposure::Trusted,
max_frames: 8,
},
);
let env = raw::Envelope::decode(st.details()).unwrap();
assert_eq!(env.frames.len(), 8);
assert!(env.frames.iter().any(|f| f.rpc == "elided"));
}
#[test]
fn public_drops_frames() {
let mut sf: ServiceFailure<Boom> = Boom { x: 0 }.into();
sf.frames.push(Frame::local(
"svc",
"rpc",
Code::Internal,
"m",
Category::System,
));
let st = encode(
&sf,
&EncodeOptions {
exposure: Exposure::Public,
max_frames: 16,
},
);
let env = raw::Envelope::decode(st.details()).unwrap();
assert!(env.frames.is_empty());
}
}