Summary
The journal records an action only after it succeeds. In registry.hpp the append sits
downstream of the call:
auto result = model.execute(action); // throws -> nothing below runs
auto resultJson = ActionTraits<Action>::resultToJson(result);
if constexpr (detail::actionLoggable<Action>() == Loggable::Yes) {
if (holder.hasActionLog()) {
holder.recordIfAttached(::morph::journal::LogEntry{ ... });
}
}
If execute throws — a validation failure, a lost connection, a rejected write — the
entry is never appended.
Why it matters
For an audit trail, the attempts that failed are often the interesting ones: who tried
to delete a record and was refused, which save was rejected and why. A log that contains
only successes cannot answer those questions, and it is not obvious from the outside that
it is doing so — the log simply looks quiet.
It also constrains the consumer side. A UI rendering a per-entry outcome badge has no
"failed" case to render, so the field is either always the same value or must be dropped.
Example
struct Report {
struct Delete {
static constexpr std::string_view Name = "Delete";
struct Payload { int id = 0; };
struct Result { bool deleted = false; };
using ReturnType = Result;
Payload payload;
};
Delete::Result execute(Delete const& a) {
if (!permitted(a.payload.id))
throw std::runtime_error{"not permitted"}; // no journal entry at all
return { .deleted = true };
}
};
Suggested direction
Wrap the call and append in both paths, with an outcome discriminator on LogEntry:
try {
auto result = model.execute(action);
record(/* outcome */ Outcome::Succeeded, resultToJson(result), /* error */ {});
return resultJson;
} catch (std::exception const& ex) {
record(Outcome::Failed, /* result */ {}, ex.what());
throw; // unchanged propagation
}
Points worth deciding:
- Opt-in or default? Recording failures changes the shape of existing logs. It could
be gated per action (alongside Loggable) or per sink.
- Error text.
what() may carry detail an app does not want persisted; a hook to map
the exception to a safe message would help.
- Sink cost. A failing action that is retried in a loop could write a lot of entries;
the existing coalesce policy may want to apply here too.
Backwards compatible if outcome defaults to "succeeded" — readers that ignore the field
behave exactly as today.
Summary
The journal records an action only after it succeeds. In
registry.hppthe append sitsdownstream of the call:
If
executethrows — a validation failure, a lost connection, a rejected write — theentry is never appended.
Why it matters
For an audit trail, the attempts that failed are often the interesting ones: who tried
to delete a record and was refused, which save was rejected and why. A log that contains
only successes cannot answer those questions, and it is not obvious from the outside that
it is doing so — the log simply looks quiet.
It also constrains the consumer side. A UI rendering a per-entry outcome badge has no
"failed" case to render, so the field is either always the same value or must be dropped.
Example
Suggested direction
Wrap the call and append in both paths, with an outcome discriminator on
LogEntry:Points worth deciding:
be gated per action (alongside
Loggable) or per sink.what()may carry detail an app does not want persisted; a hook to mapthe exception to a safe message would help.
the existing
coalescepolicy may want to apply here too.Backwards compatible if
outcomedefaults to "succeeded" — readers that ignore the fieldbehave exactly as today.