PropertyDamage.Forensics (PropertyDamage v0.2.0)
View SourceReplay production event logs through model projections for incident analysis.
When a production incident occurs, you can use Forensics to replay the actual events through your test projections. This reuses all your invariant checks and state tracking to pinpoint exactly when and how something went wrong.
Why Forensics?
Instead of manually tracing through logs, let the same models and projections that verify correctness during testing analyze production behavior:
- Same invariant checks - Production events are verified against the same assertions used in property tests
- State reconstruction - See exactly what state the system was in at each step
- Failure localization - Pinpoint the exact event that violated an invariant
- Reusable models - No need to write separate incident analysis code
Usage
# Fetch events from your observability system
{:ok, events} = ProductionLogs.fetch(trace_id: "abc123")
# Replay through model projections
result = PropertyDamage.Forensics.analyze(
events: events,
model: OrderModel,
event_mapping: MyEventMapping # Optional: translate production format
)
case result do
{:ok, %{final_state: state}} ->
IO.puts("No invariant violations detected")
{:error, failure} ->
IO.puts("Found violation at step #{failure.failure_step}")
IO.puts(PropertyDamage.Forensics.format_report(failure))
endEvent Mapping
Production events often have different field names or structures. Implement an event mapping module to translate them:
defmodule MyEventMapping do
@behaviour PropertyDamage.Forensics.EventMapping
@impl true
def map(%{"type" => "order.created", "payload" => p}) do
{:ok, %OrderCreated{order_ref: p["order_id"], amount: p["total"]}}
end
def map(_), do: :skip
endLimitations
- Events must be self-describing (contain enough context to reconstruct state)
- Assertions using
every: :commandwon't trigger (forensics has no commands) - Event ordering must match production ordering
Summary
Types
Analysis result - either success or failure.
Failed analysis result.
Successful analysis result.
Functions
Analyze a sequence of production events against a model.
Format a failure report as a human-readable string.
Generate a regression test from a forensic failure.
Types
@type analysis_result() :: {:ok, success_result()} | {:error, failure_result()}
Analysis result - either success or failure.
@type failure_result() :: %{ failure_reason: term(), failure_step: non_neg_integer(), event_at_failure: struct() | map(), state_before: map(), state_after: map(), events_leading_to_failure: [struct() | map()] }
Failed analysis result.
@type success_result() :: %{ final_state: map(), events_processed: non_neg_integer(), projections: %{required(module()) => term()} }
Successful analysis result.
Functions
@spec analyze(keyword()) :: analysis_result()
Analyze a sequence of production events against a model.
Replays events through the model's projections, running assertion checks after each event. Stops at the first invariant violation (by default).
Options
:model- The model module (required):event_mapping- Module to translate production events (optional):stop_on_first_failure- Stop at first invariant violation (default: true):projections- Override which assertion projections to use (default: model's)
Returns
{:ok, success_result}- All events processed without violations{:error, failure_result}- An invariant was violated
Examples
# Basic usage
Forensics.analyze(events: events, model: MyModel)
# With event mapping
Forensics.analyze(
events: production_events,
model: MyModel,
event_mapping: MyMapper
)
# Continue past failures
Forensics.analyze(
events: events,
model: MyModel,
stop_on_first_failure: false
)
@spec format_report(failure_result()) :: String.t()
Format a failure report as a human-readable string.
Example
{:error, failure} = Forensics.analyze(events: events, model: MyModel)
IO.puts(Forensics.format_report(failure))
# Output:
# ═══════════════════════════════════════════════════════════════════
# FORENSIC ANALYSIS: INVARIANT VIOLATION DETECTED
# ═══════════════════════════════════════════════════════════════════
#
# FAILURE OCCURRED AT: Event #47
# ...
@spec generate_regression_test(failure_result(), module()) :: String.t()
Generate a regression test from a forensic failure.
Creates Elixir code that can be added to your test suite to reproduce the failure scenario with the exact events from production.
Example
{:error, failure} = Forensics.analyze(events: events, model: MyModel)
test_code = Forensics.generate_regression_test(failure, MyModel)
File.write!("test/regressions/incident_2025_01_15_test.exs", test_code)