featherbit/debug/
render.rs1use serde::{Deserialize, Serialize};
5
6use crate::debug::diff::{diff, Change};
7use crate::debug::store::TraceSummary;
8use crate::debug::{NodeStep, Trace};
9
10#[derive(Debug, Default, Deserialize)]
13pub struct TraceFilter {
14 pub route: Option<String>,
15 pub policy: Option<String>,
16 pub status: Option<u16>,
17 pub source: Option<String>,
19 pub limit: Option<usize>,
21}
22
23fn non_empty(s: &Option<String>) -> Option<&str> {
24 s.as_deref().map(str::trim).filter(|s| !s.is_empty())
25}
26
27fn source_str(source: crate::debug::TraceSource) -> &'static str {
28 match source {
29 crate::debug::TraceSource::Request => "request",
30 crate::debug::TraceSource::Sandbox => "sandbox",
31 }
32}
33
34pub fn apply_filter(mut traces: Vec<TraceSummary>, f: &TraceFilter) -> Vec<TraceSummary> {
36 if let Some(route) = non_empty(&f.route) {
37 traces.retain(|t| t.route.as_deref() == Some(route));
38 }
39 if let Some(policy) = non_empty(&f.policy) {
40 traces.retain(|t| t.policy == policy);
41 }
42 if let Some(status) = f.status {
43 traces.retain(|t| t.status == status);
44 }
45 if let Some(source) = non_empty(&f.source) {
46 traces.retain(|t| source_str(t.source).eq_ignore_ascii_case(source));
47 }
48 if let Some(limit) = f.limit {
49 traces.truncate(limit);
50 }
51 traces
52}
53
54#[derive(Serialize)]
56struct StepWithChanges<'a> {
57 #[serde(flatten)]
58 step: &'a NodeStep,
59 changes: Vec<Change>,
60}
61
62pub fn render_trace(trace: &Trace) -> serde_json::Value {
68 let mut prev = &trace.initial;
69 let mut steps = Vec::with_capacity(trace.steps.len());
70 for step in &trace.steps {
71 steps.push(StepWithChanges {
72 step,
73 changes: diff(prev, &step.after),
74 });
75 prev = &step.after;
76 }
77 let mut out = serde_json::to_value(trace).unwrap_or_else(|_| serde_json::json!({}));
78 out["steps"] = serde_json::to_value(steps).unwrap_or_else(|_| serde_json::json!([]));
79 out
80}
81
82#[cfg(test)]
83mod tests {
84 use super::*;
85 use crate::debug::{StepOutcome, TraceSource};
86
87 fn summary(policy: &str, status: u16, source: TraceSource) -> TraceSummary {
88 TraceSummary {
89 id: format!("{policy}-{status}"),
90 seq: 1,
91 source,
92 started_ms: 0,
93 route: Some("r".into()),
94 policy: policy.into(),
95 method: "GET".into(),
96 path: "/".into(),
97 status,
98 duration_us: 1,
99 step_count: 1,
100 error_count: 0,
101 captured_bodies: false,
102 }
103 }
104
105 #[test]
106 fn filter_ands_fields_and_ignores_blank_strings() {
107 let all = vec![
108 summary("a", 200, TraceSource::Request),
109 summary("a", 500, TraceSource::Sandbox),
110 summary("b", 200, TraceSource::Request),
111 ];
112 let f = TraceFilter {
113 policy: Some("a".into()),
114 status: Some(200),
115 ..Default::default()
116 };
117 assert_eq!(apply_filter(all.clone(), &f).len(), 1);
118 let f = TraceFilter {
119 source: Some("SANDBOX".into()),
120 ..Default::default()
121 };
122 assert_eq!(apply_filter(all.clone(), &f)[0].id, "a-500");
123 let f = TraceFilter {
124 route: Some(" ".into()),
125 limit: Some(2),
126 ..Default::default()
127 };
128 assert_eq!(apply_filter(all, &f).len(), 2);
129 }
130
131 #[test]
132 fn render_attaches_changes_per_step() {
133 let ctx = crate::debug::sandbox::SandboxContextInput::default()
134 .into_context()
135 .unwrap();
136 let opts = crate::debug::CaptureOptions::default();
137 let mut rec = crate::debug::TraceRecorder::new(&ctx, opts, 10);
138 let mut after = crate::debug::sandbox::SandboxContextInput::default()
139 .into_context()
140 .unwrap();
141 after.response.status_code = 418;
142 rec.record_step(
143 "n1",
144 "echo",
145 StepOutcome::Success,
146 std::time::Duration::from_micros(5),
147 crate::debug::EdgeKind::Success,
148 Some("success"),
149 None,
150 &after,
151 );
152 let trace = rec.finish(
153 "t1".into(),
154 1,
155 TraceSource::Request,
156 None,
157 "p".into(),
158 &after,
159 std::time::Duration::from_micros(7),
160 );
161 let v = render_trace(&trace);
162 let steps = v["steps"].as_array().unwrap();
163 assert_eq!(steps.len(), 1);
164 let changes = steps[0]["changes"].as_array().unwrap();
165 assert!(
166 changes.iter().any(|c| c["path"] == "response.status_code"),
167 "{changes:?}"
168 );
169 assert_eq!(steps[0]["node_id"], "n1");
170 }
171}