# Provenance
Universal artifact lineage tracking for Elixir. Tracks what code produced what data,
across modules, queries, requests, and jobs.
Every artifact gets a deterministic `kind:identifier` ID. An ETS-backed graph store
records producer/consumer relationships. A compile tracer automatically discovers
modules and classifies them into architectural layers.
Designed for use with [Sentinel](https://github.com/fun-fx/sentinel) but works
standalone in any Elixir project.
## Installation
Add to your `mix.exs`:
```elixir
def deps do
[
{:provenance, "~> 0.1"},
{:ecto, "~> 3.10", optional: true},
{:plug, "~> 1.15", optional: true}
]
end
```
## Quick Start
### Registering Modules
Modules can be registered manually or discovered automatically via the compile tracer.
```elixir
Provenance.register_module(MyApp.Orders,
source_file: "lib/my_app/orders.ex",
layer: :context,
dependencies: ["mod:MyApp.Repo"]
)
Provenance.register_module(MyApp.Orders.Order,
source_file: "lib/my_app/orders/order.ex",
layer: :schema
)
Provenance.lookup("mod:MyApp.Orders")
# => {:ok, %{id: "mod:MyApp.Orders", kind: :mod, layer: :context, ...}}
```
### Recording Provenance
Record that an artifact was produced by another:
```elixir
Provenance.record("rec:orders:42", origin: "fn:MyApp.Orders.create/1")
Provenance.record("rec:order_items:99", origin: "fn:MyApp.Orders.add_item/2")
```
### Querying Related Artifacts
```elixir
Provenance.related("fn:MyApp.Orders.create/1")
# => ["rec:orders:42"]
Provenance.related("mod:MyApp.Orders")
# => ["mod:MyApp.Repo"]
Provenance.module_graph()
# => %{"mod:MyApp.Orders" => ["mod:MyApp.Repo"], ...}
```
### Process Context
Attach provenance context to the current process. Useful for tracing requests,
jobs, or any unit of work:
```elixir
Provenance.put_context(request_id: "req:F1a2b3c4d5", user_id: "usr:42")
Provenance.get_context()
# => %{request_id: "req:F1a2b3c4d5", user_id: "usr:42"}
# Context merges -- existing keys are preserved, new ones added
Provenance.put_context(trace_id: "trc:abc")
Provenance.get_context()
# => %{request_id: "req:F1a2b3c4d5", user_id: "usr:42", trace_id: "trc:abc"}
```
## Artifact ID Format
All IDs follow the `kind:identifier` scheme:
| Kind | Format | Example |
|------|--------|---------|
| `mod` | `mod:<Module>` | `mod:MyApp.Orders` |
| `fn` | `fn:<Module>.<function>/<arity>` | `fn:MyApp.Orders.get_order/1` |
| `tbl` | `tbl:<table>` | `tbl:orders` |
| `col` | `col:<table>.<column>` | `col:orders.total_cents` |
| `rec` | `rec:<table>:<pk>` | `rec:orders:42` |
| `req` | `req:<request_id>` | `req:F1a2b3c4d5` |
| `mig` | `mig:<migration_id>` | `mig:20240315_add_status` |
| `job` | `job:<queue>:<job_id>` | `job:default:j-123` |
| `cfg` | `cfg:<config_path>` | `cfg:app.database.pool_size` |
ID helpers:
```elixir
Provenance.module_id(MyApp.Orders) # => "mod:MyApp.Orders"
Provenance.function_id(MyApp.Orders, :get_order, 1) # => "fn:MyApp.Orders.get_order/1"
Provenance.table_id("orders") # => "tbl:orders"
Provenance.record_id("orders", 42) # => "rec:orders:42"
```
## Compile Tracer
Automatically registers modules and records inter-module dependencies at compile time.
Enable in `mix.exs`:
```elixir
def project do
[
# ...
compilers: Mix.compilers(),
tracers: [Provenance.CompileTracer]
]
end
```
Or in config:
```elixir
config :elixir, :tracers, [Provenance.CompileTracer]
```
The tracer classifies each module into an architectural layer based on file path
and module characteristics:
| Layer | Detection |
|-------|-----------|
| `:test` | Path contains `test/` |
| `:controller` | Path contains `_web/controllers/` |
| `:view` | Path contains `_web/live/` or `_web/components/` |
| `:web` | Path contains `_web/` |
| `:worker` | Path contains `/workers/` |
| `:migration` | Path contains `/migrations/` |
| `:repo` | Module name ends with `Repo` |
| `:schema` | Module exports `__schema__/1` (Ecto) |
| `:context` | Shallow lib path (depth <= 2) |
| `:lib` | Everything else |
## Architecture
```
+---------------------+
| Your Application |
+---------------------+
|
register / record / query
|
+---------------------+ +------------------------+
| Provenance API |---->| Provenance.Store |
| (lib/provenance.ex) | | (ETS GenServer) |
+---------------------+ | |
| :provenance_artifacts |
+---------------------+ | :provenance_edges |
| CompileTracer |---->| :provenance_modules |
| (compile-time hook) | +------------------------+
+---------------------+
Process Dictionary
+---------------------+
| put_context/1 | Per-process provenance context
| get_context/0 | (:provenance_context key)
+---------------------+
```
- **Provenance** -- Public API. Delegates to `Provenance.Store` for persistence
and provides ID builder functions.
- **Provenance.Store** -- GenServer owning three ETS tables: artifacts (set),
edges (duplicate_bag, bidirectional), and modules (set).
- **Provenance.CompileTracer** -- Elixir compiler tracer. On each compiled module,
registers it with layer classification and records remote function call edges.
- **Process context** -- Stored in the process dictionary. Designed for inclusion
in provenance recordings during request/job execution.
## Ecto Integration (Planned)
Ecto is an optional dependency. Current support is limited to detecting Ecto schemas
in the compile tracer for layer classification. Future versions will add:
- Automatic query-level provenance via Ecto telemetry
- Schema field to artifact ID mapping
- Migration tracking
## API Reference
### Core
| Function | Description |
|----------|-------------|
| `Provenance.register_module(module, opts)` | Register a module with metadata and dependencies |
| `Provenance.record(artifact_id, opts)` | Record provenance (`:origin` option for producer) |
| `Provenance.lookup(artifact_id)` | Look up artifact info |
| `Provenance.related(artifact_id)` | List related artifact IDs |
| `Provenance.module_graph()` | Full module dependency graph |
### ID Builders
| Function | Description |
|----------|-------------|
| `Provenance.module_id(module)` | `"mod:Module.Name"` |
| `Provenance.function_id(mod, fun, arity)` | `"fn:Mod.fun/arity"` |
| `Provenance.table_id(name)` | `"tbl:name"` |
| `Provenance.record_id(table, pk)` | `"rec:table:pk"` |
### Process Context
| Function | Description |
|----------|-------------|
| `Provenance.put_context(keyword)` | Merge attrs into process context |
| `Provenance.get_context()` | Get current process context map |
## License
MIT -- see [LICENSE](LICENSE).