Skip to content

Journal records only successful actions; failures leave no entry #23

Description

@Yaraslaut

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.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions