https://github.com/ccarvalho-eng/animus
Operational memory for Elixir and OTP systems.
https://github.com/ccarvalho-eng/animus
elixir otp
Last synced: 4 days ago
JSON representation
Operational memory for Elixir and OTP systems.
- Host: GitHub
- URL: https://github.com/ccarvalho-eng/animus
- Owner: ccarvalho-eng
- License: other
- Created: 2026-05-14T13:29:11.000Z (about 2 months ago)
- Default Branch: main
- Last Pushed: 2026-05-25T14:23:48.000Z (about 2 months ago)
- Last Synced: 2026-06-09T20:27:42.446Z (about 1 month ago)
- Topics: elixir, otp
- Language: Elixir
- Homepage:
- Size: 162 KB
- Stars: 0
- Watchers: 0
- Forks: 0
- Open Issues: 2
-
Metadata Files:
- Readme: README.md
- License: LICENSE
Awesome Lists containing this project
README
# Animus
Operational memory for Elixir and OTP systems.
[](https://github.com/ccarvalho-eng/animus/actions/workflows/ci.yml)
[](LICENSE)
[](mix.exs)
[](mix.exs)
Animus reconstructs what distributed systems experienced. It combines runtime
memories, telemetry, trace context, and process lifecycle signals into causal
timelines that help engineers understand what happened, why it happened, and
which actor or dependency propagated failure.
Animus is not another logging platform, metrics dashboard, or generic APM clone.
It is OTP-native forensic tooling for production incident reconstruction.
See [Concepts](docs/product-space.md) for the core ideas behind memories,
causality, crash inspection, and the bleeding effect.
## Why It Exists
Production incidents rarely live in one signal. A failed request may involve a
Phoenix controller, a worker process, an external timeout, retry amplification,
mailbox pressure, and a supervisor restart. Logs, metrics, and traces show
pieces. Animus preserves the operational memory that connects them.
Use Animus when you need a forensic view of an OTP incident: the timeline, the
actors involved, the pressure signals before failure, and the patterns that
remain after the trace is over.
## Quickstart
Add Animus to your application, configure the host repo, and install the
migration:
```elixir
def deps do
[
{:animus, "~> 0.1"}
]
end
```
```elixir
config :animus,
repo: MyApp.Repo,
adapters: [:phoenix, :ecto, :oban],
retention_days: 30
```
```sh
mix animus.install
mix ecto.migrate
```
After an incident, query the trace:
```elixir
Animus.timeline(trace_id)
Animus.inspect_crash(trace_id)
Animus.bleeding_effect(trace_id)
```
## Example: Payment Failure
Install automatic adapters for the signals your app already emits:
```elixir
config :animus,
repo: MyApp.Repo,
adapters: [:phoenix, :ecto, :oban]
```
```elixir
Animus.trace("payment_flow", [trace_id: request_id], fn ->
context = Animus.current_context()
{:ok, worker} = PaymentWorker.start(animus_context: context, payment: payment)
{:ok, _watch_ref} = Animus.watch(worker)
Payments.charge(payment)
end)
```
Inside a worker, restore the captured context before recording domain memories:
```elixir
Animus.with_context(animus_context, fn ->
{:ok, _} = Animus.record(:payment_worker_started, %{amount: 42})
:telemetry.execute(
[:my_app, :stripe, :timeout],
%{duration: 3_000},
%{service: "stripe", operation: "charge"}
)
{:ok, _} =
Animus.record(:stripe_timeout, %{service: "stripe", operation: "charge"},
severity: :error
)
end)
```
After the incident:
```elixir
{:ok, explanation} = Animus.explain(request_id)
explanation.narrative
# [
# "Trace payment_flow started",
# "Http request started",
# "Process watch started",
# "Payment worker started",
# "Telemetry memory",
# "Stripe timeout",
# "Request failed",
# "Trace payment_flow finished",
# "Process exited"
# ]
Enum.map(explanation.suspected_causes, & &1.memory_type)
# [:stripe_timeout, :request_failed, :process_exited]
```
The executable version of this scenario lives in
[`examples/payment_incident`](examples/payment_incident).
## Public API
Record domain memories:
```elixir
Animus.record(:checkout_failed, %{cart_id: cart.id, reason: "payment_timeout"},
severity: :error
)
```
Run a causal trace:
```elixir
Animus.trace("checkout", fn ->
Checkout.submit(cart)
end)
```
Attach automatic telemetry adapters:
```elixir
Animus.attach_adapters([:phoenix, :ecto, :oban])
Animus.detach_adapters([:phoenix, :ecto, :oban])
```
Bound adapter capture during production investigations:
```elixir
Animus.attach_adapters(
phoenix: [sample_rate: 0.25, redact_keys: [:card_number]],
ecto: [sample_rate: 0.10, max_value_size: 256],
oban: [redact_keys: [:args]]
)
```
Adapters redact common secret keys by default, including authorization headers,
passwords, tokens, cookies, and API keys. Additional `:redact_keys` are merged
with those defaults and applied through nested maps and lists before storage.
Attach custom telemetry when you need application-specific events:
```elixir
Animus.attach_telemetry(:payments, [[:my_app, :stripe, :timeout]])
```
Watch process lifecycle transitions:
```elixir
{:ok, watch_ref} = Animus.watch(MyApp.PaymentWorker)
:ok = Animus.unwatch(watch_ref)
```
Watch targeted mailbox pressure during an investigation:
```elixir
{:ok, watch_ref} = Animus.watch_mailbox(worker, threshold: 500, interval_ms: 1_000)
:ok = Animus.unwatch(watch_ref)
```
Pause and resume capture during an investigation:
```elixir
Animus.disable()
Animus.enable()
Animus.enabled?()
```
Query operational memory:
```elixir
Animus.timeline(trace_id)
Animus.causal_tree(trace_id)
Animus.explain(trace_id)
Animus.inspect_crash(trace_id)
Animus.bleeding_effect(trace_id)
```
Crash inspection gives a focused failure view:
```elixir
{:ok, inspection} = Animus.inspect_crash(request_id)
inspection.crash.memory_type
# :process_exited
Enum.map(inspection.suspected_causes, & &1.memory_type)
# [:stripe_timeout, :request_failed]
inspection.story
# "Failure point: Process exited. Suspected causes before the failure: Stripe timeout, Request failed."
```
The bleeding effect is what remains after leaving the Animus: the trace is over,
but the system carries forward what it learned. It is a deterministic summary of
stats, patterns, suspected causes, and the same crash story without involving AI.
```text
The incident has ended.
The memory has not.
What the system lived through becomes what the engineer can know.
```
```elixir
{:ok, effect} = Animus.bleeding_effect(request_id)
effect.memory_type_counts["mailbox_warning"]
# 1
Enum.map(effect.patterns, & &1.type)
# [:mailbox_pressure_before_crash, :external_timeout_before_crash, :process_crash]
```
Time-travel by incident window:
```elixir
Animus.memories_on(~D[2026-05-14])
Animus.memories_between(
~U[2026-05-14 13:00:00Z],
~U[2026-05-14 14:00:00Z]
)
```
## Installation
Install the host-app migration:
```sh
mix animus.install
mix ecto.migrate
```
Animus stores memories in `animus_memories`, not a generic `memories` table.
Retention defaults to 30 days and is configurable:
```elixir
config :animus,
repo: MyApp.Repo,
enabled: true,
retention_days: 30,
retention_interval_ms: :timer.hours(1)
```
Animus uses the host application repo. It does not start or own a database
connection pool.
Use `enabled: false` if you want instrumentation installed but capture paused
until an investigation starts.
## Operator Telemetry
Animus emits its own telemetry when capture degrades or changes payloads before
storage. Attach these events if you want alerts or counters for Animus itself:
```elixir
[
[:animus, :memory, :dropped],
[:animus, :memory, :sink_failed],
[:animus, :adapter, :sampled_out],
[:animus, :adapter, :payload_redacted],
[:animus, :adapter, :payload_truncated]
]
```
These events include counts and safe metadata such as `:memory_type`, `:source`,
`:adapter`, `:event_name`, `:reason`, and redacted key names. They do not include
memory payload values.
## Safety Defaults
Animus is designed for incident reconstruction, not unbounded tracing.
- Uses the host application repo; no separate database pool.
- Stores memories in `animus_memories`.
- Keeps a bounded retention window by default.
- Captures common framework telemetry through explicit adapters.
- Lets adapters sample, redact sensitive keys, and truncate oversized values.
- Keeps trace and watcher capture best-effort so sink failures do not block app code.
- Emits operator telemetry when capture is dropped, sampled, redacted, truncated, or sink writes fail.
- Watches only the processes and mailboxes you choose.
- Does not globally trace BEAM messages.
- Does not use AI to invent causes.
## Current Capabilities
Animus currently provides:
- Memory ingestion
- Telemetry integration
- Trace context propagation
- Process lifecycle monitoring
- Targeted mailbox pressure monitoring
- Timeline reconstruction
- Causal explanations and crash inspection
- Bleeding effect summaries for stats and patterns
- Lightweight API
- Host-repo Postgres persistence
- Configurable retention
Animus does not currently provide:
- AI anomaly detection
- Full BEAM message tracing
- Distributed replay
- Machine learning
- Predictive analysis
- Fancy dashboards
- Multi-language support
See [Current capabilities](docs/v1-scope.md) for the library boundary.
## Development
```sh
mix precommit
mix examples.smoke
```
The payment incident example is executable and runs as part of `mix precommit`.
## License
Apache-2.0. See [LICENSE](LICENSE).