Giving every reporter what it needs
What you want to do
The logging service files lines by customer, and the shop needs the bank's receipt to refund an order. You want every line of the log to name the customer, and the receipt once the bank has given it, even when the journey stops right after.
The code
The shop is chapter 31's, without chapter 32's DebugReporter. main gives each instance the customer's name with reporters_global_metadata, which makes it an entry of the journey's global metadata: data for its reporters, which no step reads:
let instance = Orders::builder()
.channel("email")
.reporters_global_metadata("customer", customer.to_owned())
.initial_data(NewOrder {
order: order.to_owned(),
customer: customer.to_owned(),
price,
discount,
parcel: Parcel { weight, size },
card: card.map(str::to_owned),
gift_card,
})
.create()?;Ok(Orders::builder()
.channel("email")
.reporters_global_metadata("customer", customer.to_owned())
.initial_data(NewOrder {
order: order.to_owned(),
customer: customer.to_owned(),
price,
discount,
parcel,
card: Some(card.to_owned()),
gift_card: false,
})
.create()?)Orders is the workflow chapter 23 declares, NewOrder is as chapter 25 shows it and Parcel as chapter 21 does.
The charge asks for its step reporter, and sets the receipt as an entry as soon as the bank gives it, before contributing it:
impl Charge {
fn run(
self,
#[service] payments: &dyn Payments,
#[contributor] mut contributor: Contributor<'_>,
#[reporter] mut reporter: StepReporter<'_>,
) -> Result<Outcome, Error> {
if self.amount <= 0 {
return Self::fail("nothing-to-charge");
}
let Some(receipt) = payments.charge(&self.order, &self.card, self.amount)? else {
return Self::fail("card-expired");
};
reporter.set_global_metadata("receipt", receipt.clone())?;
contributor.contribute("receipt", receipt);
if self.gift_card {
contributor.contribute("code", format!("G-{}", self.order));
}
Self::succeed()
}
}impl Charge {
async fn run(
self,
#[service] payments: &dyn Payments,
#[contributor] mut contributor: Contributor<'_>,
#[reporter] mut reporter: AsyncStepReporter<'_>,
) -> Result<Outcome, Error> {
if self.amount <= 0 {
return Self::fail("nothing-to-charge");
}
let Some(receipt) = payments
.charge(&self.order, &self.card, self.amount)
.await?
else {
return Self::fail("card-expired");
};
reporter
.set_global_metadata("receipt", receipt.clone())
.await?;
contributor.contribute("receipt", receipt);
if self.gift_card {
contributor.contribute("code", format!("G-{}", self.order));
}
Self::succeed()
}
}The payment client it asks for is chapter 14's Payments, and its asynchronous form chapter 26's.
In the asynchronous form, the charge's step reporter is an AsyncStepReporter, and set_global_metadata is awaited.
The log keeps each entry it receives in a GlobalMetadata, from itinera::report, through the method set_global_metadata of Reporter. It starts each line with the customer's name and, once there is one, the receipt:
/// The shop's log: it writes a line for every event of a journey to the logging service, starting
/// with the workflow's name, the journey's ID, the customer's name and, once the bank has given
/// it, the receipt. A line the service refuses fails the report.
#[derive(Default)]
struct Log {
service: LoggingService,
metadata: GlobalMetadata,
}
impl Reporter for Log {
fn set_global_metadata(&mut self, key: &str, value: &AnyValue) -> Result<(), Error> {
self.metadata.insert(key, value.clone());
Ok(())
}
fn report(&mut self, event: &Event) -> Result<(), Error> {
let line = format!(
"log {} {} {} #{} {}",
event.workflow,
event.journey_id,
self.whose(),
event.sequence,
event.kind()
);
let line = match detail(&event.body) {
Some(detail) => format!("{line} {detail}"),
None => line,
};
self.service.write(line)?;
Ok(())
}
}
impl Log {
/// The customer the journey is for and, once the bank has given it, the charge's receipt.
fn whose(&self) -> String {
let customer = self.text("customer").unwrap_or("someone");
match self.text("receipt") {
Some(receipt) => format!("for {customer}, receipt {receipt}"),
None => format!("for {customer}"),
}
}
/// The entry of `key`, when it is text.
fn text(&self, key: &str) -> Option<&str> {
let value = self.metadata.get(key)?;
value.downcast_ref::<String>().map(String::as_str)
}
}/// The shop's log: it writes a line for every event of a journey to the logging service, starting
/// with the workflow's name, the journey's ID, the customer's name and, once the bank has given
/// it, the receipt. A line the service refuses fails the report.
#[derive(Default)]
struct Log {
service: LoggingService,
metadata: GlobalMetadata,
}
impl AsyncReporter for Log {
fn set_global_metadata(&mut self, key: &str, value: &AnyValue) -> Result<(), Error> {
self.metadata.insert(key, value.clone());
Ok(())
}
async fn report(&mut self, event: &Event) -> Result<(), Error> {
let line = format!(
"log {} {} {} #{} {}",
event.workflow,
event.journey_id,
self.whose(),
event.sequence,
event.kind()
);
let line = match detail(&event.body) {
Some(detail) => format!("{line} {detail}"),
None => line,
};
self.service.write(line).await?;
Ok(())
}
}
impl Log {
/// The customer the journey is for and, once the bank has given it, the charge's receipt.
fn whose(&self) -> String {
let customer = self.text("customer").unwrap_or("someone");
match self.text("receipt") {
Some(receipt) => format!("for {customer}, receipt {receipt}"),
None => format!("for {customer}"),
}
}
/// The entry of `key`, when it is text.
fn text(&self, key: &str) -> Option<&str> {
let value = self.metadata.get(key)?;
value.downcast_ref::<String>().map(String::as_str)
}
}detail is the function the log has had since chapter 22, with one more arm, which names the key of global_metadata_set:
EventBody::GlobalMetadataSet { key, .. } => Some(key.clone()),set_global_metadata stays synchronous on an AsyncReporter too: it only keeps the entry for the events that follow. main runs Ann's and Pat's orders, and prints:
Ann's order:
log orders A-1 for Ann #1 journey_started card, customer, discount, gift_card, order, parcel, price
log orders A-1 for Ann #2 attempt_started lookup, attempt 1
log orders A-1 for Ann #3 service_provided lookup gets order_book from ledger, a ch33_giving_every_reporter_what_it_needs::Ledger
the order book has no order A-1 yet
log orders A-1 for Ann #4 step_succeeded lookup
log orders A-1 for Ann #5 contribution_committed completed
log orders A-1 for Ann #6 hook_called already_completed: on step success of lookup
log orders A-1 for Ann #7 attempt_started price, attempt 1
log orders A-1 for Ann #8 step_succeeded price
log orders A-1 for Ann #9 contribution_committed amount
log orders A-1 for Ann #10 attempt_started charge, attempt 1
log orders A-1 for Ann #11 service_provided charge gets payments from bank, a ch33_giving_every_reporter_what_it_needs::Bank
the bank charges 30 for order A-1
log orders A-1 for Ann, receipt R-A-1 #12 global_metadata_set receipt
log orders A-1 for Ann, receipt R-A-1 #13 step_succeeded charge
log orders A-1 for Ann, receipt R-A-1 #14 contribution_committed receipt
log orders A-1 for Ann, receipt R-A-1 #15 hook_called audit: on step success of charge
log orders A-1 for Ann, receipt R-A-1 #16 contribution_committed audit
log orders A-1 for Ann, receipt R-A-1 #17 attempt_started ship, attempt 1
log orders A-1 for Ann, receipt R-A-1 #18 input_adapter_supplied ship gets weight from parcels
log orders A-1 for Ann, receipt R-A-1 #19 service_provided ship gets carrier from carriers, a ch33_giving_every_reporter_what_it_needs::MainCarrier
the main carrier books order A-1
log orders A-1 for Ann, receipt R-A-1 #20 step_info ship: the parcel leaves with receipt R-A-1
log orders A-1 for Ann, receipt R-A-1 #21 step_succeeded ship
log orders A-1 for Ann, receipt R-A-1 #22 contribution_committed tracking
log orders A-1 for Ann, receipt R-A-1 #23 optional_input_absent code
email to Ann: Order A-1 is on its way, tracking T-A-1.
log orders A-1 for Ann, receipt R-A-1 #24 hook_called notify: on workflow success
log orders A-1 for Ann, receipt R-A-1 #25 journey_succeeded
the journey succeeded, with receipt R-A-1 and tracking T-A-1
audit: charge took 30, receipt R-A-1
Pat's order:
log orders A-15 for Pat #1 journey_started card, customer, gift_card, order, parcel, price
log orders A-15 for Pat #2 attempt_started lookup, attempt 1
log orders A-15 for Pat #3 service_provided lookup gets order_book from ledger, a ch33_giving_every_reporter_what_it_needs::Ledger
the order book has no order A-15 yet
log orders A-15 for Pat #4 step_succeeded lookup
log orders A-15 for Pat #5 contribution_committed completed
log orders A-15 for Pat #6 hook_called already_completed: on step success of lookup
log orders A-15 for Pat #7 attempt_started price, attempt 1
log orders A-15 for Pat #8 optional_input_absent discount
log orders A-15 for Pat #9 step_succeeded price
log orders A-15 for Pat #10 contribution_committed amount
log orders A-15 for Pat #11 attempt_started charge, attempt 1
log orders A-15 for Pat #12 service_provided charge gets payments from bank, a ch33_giving_every_reporter_what_it_needs::Bank
the bank charges 50 for order A-15
log orders A-15 for Pat, receipt R-A-15 #13 global_metadata_set receipt
the journey was aborted (reporter failed: the logging service is down)log orders A-15 for Pat #1 journey_started card, customer, gift_card, order, parcel, price
log orders A-1 for Ann #1 journey_started card, customer, discount, gift_card, order, parcel, price
log orders A-1 for Ann #2 attempt_started lookup, attempt 1
log orders A-15 for Pat #2 attempt_started lookup, attempt 1
log orders A-15 for Pat #3 service_provided lookup gets order_book from ledger, a ch33_giving_every_reporter_what_it_needs_async::Ledger
the order book has no order A-15 yet
log orders A-1 for Ann #3 service_provided lookup gets order_book from ledger, a ch33_giving_every_reporter_what_it_needs_async::Ledger
the order book has no order A-1 yet
log orders A-1 for Ann #4 step_succeeded lookup
log orders A-15 for Pat #4 step_succeeded lookup
log orders A-15 for Pat #5 contribution_committed completed
log orders A-1 for Ann #5 contribution_committed completed
log orders A-1 for Ann #6 hook_called already_completed: on step success of lookup
log orders A-15 for Pat #6 hook_called already_completed: on step success of lookup
log orders A-15 for Pat #7 attempt_started price, attempt 1
log orders A-1 for Ann #7 attempt_started price, attempt 1
log orders A-1 for Ann #8 step_succeeded price
log orders A-15 for Pat #8 optional_input_absent discount
log orders A-15 for Pat #9 step_succeeded price
log orders A-1 for Ann #9 contribution_committed amount
log orders A-1 for Ann #10 attempt_started charge, attempt 1
log orders A-15 for Pat #10 contribution_committed amount
log orders A-15 for Pat #11 attempt_started charge, attempt 1
log orders A-1 for Ann #11 service_provided charge gets payments from bank, a ch33_giving_every_reporter_what_it_needs_async::Bank
the bank charges 30 for order A-1
log orders A-15 for Pat #12 service_provided charge gets payments from bank, a ch33_giving_every_reporter_what_it_needs_async::Bank
the bank charges 50 for order A-15
log orders A-1 for Ann, receipt R-A-1 #12 global_metadata_set receipt
log orders A-1 for Ann, receipt R-A-1 #13 step_succeeded charge
log orders A-15 for Pat, receipt R-A-15 #13 global_metadata_set receipt
log orders A-1 for Ann, receipt R-A-1 #14 contribution_committed receipt
log orders A-1 for Ann, receipt R-A-1 #15 hook_called audit: on step success of charge
log orders A-1 for Ann, receipt R-A-1 #16 contribution_committed audit
log orders A-1 for Ann, receipt R-A-1 #17 attempt_started ship, attempt 1
log orders A-1 for Ann, receipt R-A-1 #18 input_adapter_supplied ship gets weight from parcels
log orders A-1 for Ann, receipt R-A-1 #19 service_provided ship gets carrier from carriers, a ch33_giving_every_reporter_what_it_needs_async::MainCarrier
the main carrier books order A-1
log orders A-1 for Ann, receipt R-A-1 #20 step_info ship: the parcel leaves with receipt R-A-1
log orders A-1 for Ann, receipt R-A-1 #21 step_succeeded ship
log orders A-1 for Ann, receipt R-A-1 #22 contribution_committed tracking
log orders A-1 for Ann, receipt R-A-1 #23 optional_input_absent code
email to Ann: Order A-1 is on its way, tracking T-A-1.
log orders A-1 for Ann, receipt R-A-1 #24 hook_called notify: on workflow success
log orders A-1 for Ann, receipt R-A-1 #25 journey_succeeded
order A-1 succeeded
order A-15 abortedHow it works
- Global metadata is for the reporters. Each entry is a key and a value, which every reporter of the journey receives. Steps and hooks never read it, so it changes no decision.
- The entries come before the first event. The journey ID comes first, under the key
journey_id, then the instance's entries, in the order they were set. So Pat's first line,journey_started, already names Pat. The keyjourney_idis the executor's own. - A reporter does what it wants with an entry. By default,
set_global_metadatadoes nothing, which is why the desk is unchanged. The log keeps the entries, and reads them back withget, as anAnyValuethatdowncast_refreads asString. - A step can set an entry while it runs. Every reporter receives it, then the journey emits
global_metadata_set, which names the key and who set it, but not the value, all beforeset_global_metadatareturns. The log wrote that line with the receipt in it, event 13, before it failed on the charge's success. - An entry stands whatever happens after. Pat's receipt never reached the data bag, since the journey was aborted first, but the log had it. So the shop finds Pat's payment,
R-A-15, in the log's last line, and can refund it.
Every line of the log now says whose order it is and, from the charge on, how it was paid. But the audit's line, which a hook writes, says neither which journey nor which attempt took the money.
Outcomes
We have shown how a journey's reporters receive data of their own, from the instance and from the steps, as global metadata.
| The usual way | With Itinera |
|---|---|
| A value every log line needs, such as a customer's name, is often passed down through every call, or kept in a global variable. | The instance gives it once to every reporter, before the first event. |
| A value a log line needs, found halfway through a request, is often lost when the request crashes right after. | A step sets it for every reporter as soon as it has it, and it stands whatever happens next. |