use crate::base::{Change, Context, Error, ErrorKind, ErrorRecord, TaskIx, WorkflowIx};
use crate::expr::{Path, path};
use datavalue::OwnedDataValue;
use serde::{Deserialize, Deserializer, Serialize, Serializer};
use std::sync::Arc;
use std::time::Duration;
#[derive(Clone, Debug)]
#[non_exhaustive]
pub struct TraceOptions {
pub timings: bool,
pub changes: bool,
pub redact: Vec<Path>,
pub max_bytes: Option<usize>,
}
impl Default for TraceOptions {
fn default() -> Self {
Self {
timings: false,
changes: true,
redact: Vec::new(),
max_bytes: None,
}
}
}
impl TraceOptions {
#[must_use]
pub fn timings(mut self, on: bool) -> Self {
self.timings = on;
self
}
#[must_use]
pub fn changes(mut self, on: bool) -> Self {
self.changes = on;
self
}
#[must_use]
pub fn redact(mut self, paths: Vec<Path>) -> Self {
self.redact = paths;
self
}
#[must_use]
pub fn max_bytes(mut self, max: usize) -> Self {
self.max_bytes = Some(max);
self
}
}
#[derive(Clone, Debug, Serialize, Deserialize)]
#[cfg_attr(feature = "ts", derive(ts_rs::TS))]
#[non_exhaustive]
pub struct Trace {
pub initial: Context,
pub steps: Vec<TraceStep>,
pub truncated: bool,
}
impl Trace {
pub fn contexts(&self) -> impl Iterator<Item = Context> + '_ {
let mut root = self.initial.as_value().clone();
self.steps.iter().map(move |step| {
for change in &step.changes {
apply(&mut root, change);
}
Context::from_root(root.clone())
})
}
pub fn mapping_contexts(&self, step: usize) -> Option<Vec<Context>> {
let at = self.steps.get(step)?;
if at.mapping_ends.is_empty() || !valid_mapping_ends(at) {
return None;
}
let mut root = self.initial.as_value().clone();
for earlier in &self.steps[..step] {
for change in &earlier.changes {
apply(&mut root, change);
}
}
let mut out = Vec::with_capacity(at.mapping_ends.len());
let mut from = 0usize;
for &end in &at.mapping_ends {
let end = end as usize;
for change in &at.changes[from..end] {
apply(&mut root, change);
}
out.push(Context::from_root(root.clone()));
from = end;
}
Some(out)
}
pub fn validate(&self) -> crate::base::Result<()> {
match self.initial.as_value() {
OwnedDataValue::Object(roots) => {
for (root, value) in roots {
check_root(root, value)?;
}
}
_ => {
return Err(Error::new(
ErrorKind::Config,
"a trace's initial context is not an object of roots",
));
}
}
for (i, step) in self.steps.iter().enumerate() {
if !valid_mapping_ends(step) {
return Err(Error::new(
ErrorKind::Config,
format!(
"step {i}'s mapping_ends {:?} do not fit its {} change(s)",
step.mapping_ends,
step.changes.len()
),
));
}
for change in &step.changes {
let at = change.path.parse::<Path>().map_err(|_| {
Error::new(
ErrorKind::Config,
format!(
"step {i}'s change path {:?} is not a context path",
change.path
),
)
})?;
if !at.is_root() {
continue;
}
if change.removed {
return Err(Error::new(
ErrorKind::Config,
format!(
"step {i} removes the root `{}`, which a context always keeps",
change.path
),
));
}
check_root(&change.path, &change.new_value)?;
}
}
Ok(())
}
}
pub(super) fn valid_mapping_ends(step: &TraceStep) -> bool {
let mut before = 0u32;
for &end in &step.mapping_ends {
if end < before || usize::try_from(end).map_or(true, |e| e > step.changes.len()) {
return false;
}
before = end;
}
true
}
fn check_root(name: &str, value: &OwnedDataValue) -> crate::base::Result<()> {
if matches!(value, OwnedDataValue::Object(_) | OwnedDataValue::Null) {
return Ok(());
}
Err(Error::new(
ErrorKind::Config,
format!("`{name}` is a context root and holds neither an object nor null"),
))
}
pub(super) fn apply(root: &mut OwnedDataValue, change: &Change) {
let Ok(at) = change.path.parse::<Path>() else {
return;
};
if change.removed {
path::remove(root, at.segs());
return;
}
if at.is_root() && !change.replaced {
path::merge_root(root, at.root().as_str(), change.new_value.clone());
return;
}
at.assign(root, change.new_value.clone());
}
#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)]
#[cfg_attr(feature = "ts", derive(ts_rs::TS))]
#[serde(rename_all = "lowercase")]
#[non_exhaustive]
pub enum StepResult {
Executed,
Skipped,
Failed,
}
#[derive(Clone, Debug, Serialize, Deserialize)]
#[cfg_attr(feature = "ts", derive(ts_rs::TS))]
#[non_exhaustive]
pub struct TraceStep {
pub workflow: WorkflowIx,
pub workflow_id: Arc<str>,
pub task: Option<TaskIx>,
pub task_id: Option<Arc<str>>,
pub result: StepResult,
pub status: Option<u16>,
pub changes: Vec<Change>,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub mapping_ends: Vec<u32>,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub errors: Vec<ErrorRecord>,
#[serde(
rename = "elapsed_us",
default,
serialize_with = "micros",
deserialize_with = "from_micros",
skip_serializing_if = "Option::is_none"
)]
#[cfg_attr(feature = "ts", ts(type = "number", optional))]
pub elapsed: Option<Duration>,
#[serde(skip_serializing_if = "Option::is_none")]
#[cfg_attr(feature = "ts", ts(optional))]
pub loop_counter: Option<i64>,
#[serde(skip_serializing_if = "Option::is_none")]
#[cfg_attr(feature = "ts", ts(optional))]
pub element_index: Option<usize>,
}
impl TraceStep {
pub(crate) fn new(workflow: WorkflowIx, id: &Arc<str>, result: StepResult) -> Self {
Self {
workflow,
workflow_id: Arc::clone(id),
task: None,
task_id: None,
result,
status: None,
changes: Vec::new(),
mapping_ends: Vec::new(),
errors: Vec::new(),
elapsed: None,
loop_counter: None,
element_index: None,
}
}
}
fn micros<S: Serializer>(elapsed: &Option<Duration>, s: S) -> Result<S::Ok, S::Error> {
match elapsed {
Some(d) => s.serialize_u64(u64::try_from(d.as_micros()).unwrap_or(u64::MAX)),
None => s.serialize_none(),
}
}
fn from_micros<'de, D: Deserializer<'de>>(d: D) -> Result<Option<Duration>, D::Error> {
Ok(Option::<u64>::deserialize(d)?.map(Duration::from_micros))
}
#[cfg(test)]
mod tests {
use super::*;
use serde_json::json;
fn value(v: serde_json::Value) -> OwnedDataValue {
OwnedDataValue::from(&v)
}
fn write(path: &str, old: serde_json::Value, new: serde_json::Value) -> Change {
Change::write(path, value(old), value(new))
}
fn trace() -> Trace {
let id: Arc<str> = Arc::from("w");
let mut earlier = TraceStep::new(WorkflowIx::new(0), &id, StepResult::Executed);
earlier.changes = vec![write("data.z", json!(null), json!(0))];
let mut map = TraceStep::new(WorkflowIx::new(0), &id, StepResult::Executed);
map.changes = vec![
write("data.a", json!(null), json!(1)),
write("data.list.0", json!(null), json!("x")),
write("data.list.1", json!(null), json!("y")),
write("data", json!({}), json!({"b": 2, "c": 3})),
write(
"metadata.progress",
json!(null),
json!({"workflow_id": "w", "task_id": "m", "status_code": 200}),
),
];
map.mapping_ends = vec![1, 1, 3, 4];
Trace {
initial: Context::from_root(value(json!({
"data": {}, "metadata": {}, "temp_data": {}
}))),
steps: vec![earlier, map],
truncated: false,
}
}
#[test]
fn a_map_step_replays_mapping_by_mapping_from_the_steps_before_it() {
let trace = trace();
let contexts = trace.mapping_contexts(1).expect("a map step");
let data: Vec<serde_json::Value> = contexts
.iter()
.map(|c| serde_json::to_value(c.data()).unwrap())
.collect();
assert_eq!(
data,
vec![
json!({"z": 0, "a": 1}),
json!({"z": 0, "a": 1}),
json!({"z": 0, "a": 1, "list": ["x", "y"]}),
json!({"z": 0, "a": 1, "list": ["x", "y"], "b": 2, "c": 3}),
]
);
let after = trace.contexts().nth(1).unwrap();
let last = contexts.last().unwrap();
assert_eq!(last.data(), after.data());
assert!(last.metadata().as_object().is_some_and(|m| m.is_empty()));
assert_eq!(
serde_json::to_value(after.metadata()).unwrap()["progress"]["status_code"],
json!(200)
);
}
#[test]
fn a_step_without_mapping_ends_and_one_out_of_range_give_none() {
let trace = trace();
assert!(trace.mapping_contexts(0).is_none());
assert!(trace.mapping_contexts(2).is_none());
}
#[test]
fn a_malformed_mapping_end_gives_none() {
let mut past = trace();
past.steps[1].mapping_ends = vec![1, 6];
assert!(past.mapping_contexts(1).is_none());
let mut backwards = trace();
backwards.steps[1].mapping_ends = vec![3, 1];
assert!(backwards.mapping_contexts(1).is_none());
}
#[test]
fn validate_refuses_what_a_replay_cannot_stand() {
assert!(trace().validate().is_ok());
let mut scalar = trace();
scalar.steps[1]
.changes
.push(Change::write("metadata", value(json!({})), value(json!(5))));
assert!(scalar.validate().is_err());
let mut removed = trace();
removed.steps[0].changes = vec![Change::removal("data", value(json!({})))];
assert!(removed.validate().is_err());
let mut bad_path = trace();
bad_path.steps[0].changes = vec![Change::write(
"outside",
value(json!(null)),
value(json!(1)),
)];
assert!(bad_path.validate().is_err());
let mut bad_initial = trace();
bad_initial.initial = Context::from_root(value(json!({
"data": 5, "metadata": {}, "temp_data": {}
})));
assert!(bad_initial.validate().is_err());
let mut redacted = trace();
redacted.steps[0].changes =
vec![Change::write("data", value(json!({})), value(json!(null)))];
assert!(redacted.validate().is_ok());
}
}