Packages
provenance
0.1.0
Universal artifact lineage graph for Elixir. Tracks what code produced what data, across modules, queries, requests, and jobs.
Current section
Files
Jump to
Current section
Files
lib/provenance.ex
defmodule Provenance do
@moduledoc """
Universal artifact lineage graph for Elixir.
Tracks what code produced what data -- across modules, queries, requests,
and background jobs. Every artifact gets a deterministic, searchable ID.
## Quick start
# Query the module graph
{:ok, info} = Provenance.lookup("mod:MyApp.Orders")
info.layer #=> :context
info.source_file #=> "lib/my_app/orders.ex"
info.dependencies #=> ["mod:MyApp.Orders.Order", "mod:MyApp.Repo"]
# Record provenance for an operation
Provenance.record("rec:orders:42", origin: "fn:MyApp.Orders.create_order/1")
# Query: what code touches this table?
Provenance.related("tbl:orders")
#=> ["mod:MyApp.Orders", "mod:MyApp.Orders.Order", "mig:20240315_add_status"]
## Provenance IDs
Every artifact is identified by a `kind:identifier` string:
- `mod:MyApp.Orders` -- a module
- `fn:MyApp.Orders.get_order/1` -- a function
- `tbl:orders` -- a database table
- `rec:orders:42` -- a specific record
- `req:F1a2b3c4d5` -- an HTTP request
"""
alias Provenance.Store
@type artifact_id :: String.t()
@type artifact_info :: %{
id: artifact_id(),
kind: atom(),
layer: atom() | nil,
source_file: String.t() | nil,
metadata: map()
}
@doc "Look up an artifact by its provenance ID."
@spec lookup(artifact_id()) :: {:ok, artifact_info()} | :not_found
def lookup(id) when is_binary(id) do
Store.lookup(id)
end
@doc "Record a provenance relationship: `artifact_id` was produced by `origin`."
@spec record(artifact_id(), keyword()) :: :ok
def record(artifact_id, opts \\ []) when is_binary(artifact_id) do
origin = Keyword.get(opts, :origin)
metadata = Keyword.get(opts, :metadata, %{})
Store.record(artifact_id, origin, metadata)
end
@doc "Find all artifacts related to the given ID (producers, consumers, co-located)."
@spec related(artifact_id()) :: [artifact_id()]
def related(id) when is_binary(id) do
Store.related(id)
end
@doc "Register a module with its metadata (called by the compile tracer)."
@spec register_module(module(), keyword()) :: :ok
def register_module(module, opts \\ []) when is_atom(module) do
Store.register_module(module, opts)
end
@doc "Get the full module dependency graph."
@spec module_graph() :: %{artifact_id() => [artifact_id()]}
def module_graph do
Store.module_graph()
end
@doc "Build a provenance ID for a module."
@spec module_id(module()) :: artifact_id()
def module_id(module) when is_atom(module) do
"mod:#{inspect(module)}"
end
@doc "Build a provenance ID for a function."
@spec function_id(module(), atom(), non_neg_integer()) :: artifact_id()
def function_id(module, function, arity)
when is_atom(module) and is_atom(function) and is_integer(arity) do
"fn:#{inspect(module)}.#{function}/#{arity}"
end
@doc "Build a provenance ID for a table."
@spec table_id(String.t()) :: artifact_id()
def table_id(table) when is_binary(table), do: "tbl:#{table}"
@doc "Build a provenance ID for a record."
@spec record_id(String.t(), term()) :: artifact_id()
def record_id(table, pk), do: "rec:#{table}:#{pk}"
@doc """
Attach provenance context to the current process.
This context is automatically included in any provenance recordings
made within the process (e.g., Ecto queries, log entries).
"""
@spec put_context(keyword()) :: :ok
def put_context(attrs) when is_list(attrs) do
existing = Process.get(:provenance_context, %{})
Process.put(:provenance_context, Map.merge(existing, Map.new(attrs)))
:ok
end
@doc "Get the provenance context for the current process."
@spec get_context() :: map()
def get_context do
Process.get(:provenance_context, %{})
end
end