diff --git a/README.md b/README.md index 8c4de05..8a57136 100644 --- a/README.md +++ b/README.md @@ -75,7 +75,7 @@ defmodule MyApp.Version do field :recorded_at, :utc_datetime # custom fields - has_one :actor, MyApp.User + belongs_to :actor, MyApp.User end def changeset(struct, params \\ %{}) do @@ -119,8 +119,6 @@ end ### Recording custom data -**TBD: not implemented yet** - If you want to track custom data such as the user id, you can simply pass a keyword list with that data to the `:ex_audit_custom` option in any Repo function: @@ -151,6 +149,14 @@ in the plug tree. In some cases where it is not possible to call the Repo function from the conn process, you have to pass the custom data manually via the options described above. +Examples for data you might want to track additionally: + + * User ID + * API Key ID + * Message from the user describing what she changed + ## More -The documentation is at [https://hexdocs.pm/ex_audit](https://hexdocs.pm/ex_audit). +The documentation is available at [https://hexdocs.pm/ex_audit](https://hexdocs.pm/ex_audit). **TBD: release documentation on hexdocs.pm** + +Check out [ZENNER IoT Solutions](https://zenner-iot.com/), makers of the [ELEMENT IoT platform](https://zenner-iot.com/iot-plattform). \ No newline at end of file diff --git a/example/version.ex b/example/version.ex index 554e4bd..145d1ec 100644 --- a/example/version.ex +++ b/example/version.ex @@ -10,7 +10,7 @@ defmodule ExAudit.Test.Version do field :recorded_at, :utc_datetime # custom fields - has_one :actor, Test.User + belongs_to :actor, ExAudit.Test.User end def changeset(struct, params \\ %{}) do diff --git a/lib/ex_audit.ex b/lib/ex_audit.ex index a06c0e9..5ca40a2 100644 --- a/lib/ex_audit.ex +++ b/lib/ex_audit.ex @@ -1,4 +1,18 @@ defmodule ExAudit do + use Application + + @tracked_data_table :tracked_data_table + + def start(_, _) do + import Supervisor.Spec + + children = [ + worker(ExAudit.CustomData, []) + ] + + opts = opts = [strategy: :one_for_one, name: ExAudit.Supervisor] + Supervisor.start_link(children, opts) + end @moduledoc """ # Configuration @@ -15,6 +29,6 @@ defmodule ExAudit do end def track_pid(pid, data) do - # TODO + ExAudit.CustomData.track(pid, data) end end diff --git a/lib/tracking/custom_data.ex b/lib/tracking/custom_data.ex new file mode 100644 index 0000000..633f7f3 --- /dev/null +++ b/lib/tracking/custom_data.ex @@ -0,0 +1,44 @@ +defmodule ExAudit.CustomData do + use GenServer + + def start_link() do + GenServer.start_link(__MODULE__, nil, name: __MODULE__) + end + + def init(nil) do + ets = :ets.new(:track_by_pid, [:public]) + {:ok, ets} + end + + def track(pid, data) do + GenServer.call(__MODULE__, {:store, pid, data}) + end + + def handle_call({:store, pid, data}, _, ets) do + :ets.insert(ets, {pid, data}) + Process.monitor(pid) + {:reply, :ok, ets} + end + + def handle_call({:get, pid}, _, ets) do + case :ets.lookup(ets, pid) do + [] -> {:reply, [], ets} + list -> + values = Enum.flat_map(list, &elem(&1, 1)) + {:reply, values, ets} + end + end + + def get() do + GenServer.call(__MODULE__, {:get, self()}) + end + + def get(pid) do + GenServer.call(__MODULE__, {:get, pid}) + end + + def handle_info({:DOWN, _, :process, pid, _}, ets) do + :ets.delete(ets, pid) + {:noreply, ets} + end +end \ No newline at end of file diff --git a/lib/tracking/tracking.ex b/lib/tracking/tracking.ex index 939187a..b6b9ba5 100644 --- a/lib/tracking/tracking.ex +++ b/lib/tracking/tracking.ex @@ -48,7 +48,13 @@ defmodule ExAudit.Tracking do changes = find_changes(action, changeset, resulting_struct) now = DateTime.utc_now - custom_fields = Keyword.get(opts, :ex_audit_custom, []) |> Enum.into(%{}) + custom_fields_opts = + Keyword.get(opts, :ex_audit_custom, []) + |> Enum.into(%{}) + custom_fields_process = + ExAudit.CustomData.get() + |> Enum.into(%{}) + custom_fields = Map.merge(custom_fields_process, custom_fields_opts) changes = Enum.map(changes, fn change -> change = Map.put(change, :recorded_at, now) diff --git a/mix.exs b/mix.exs index 87c7fff..882007a 100644 --- a/mix.exs +++ b/mix.exs @@ -26,6 +26,7 @@ defmodule ExAudit.Mixfile do # Run "mix help compile.app" to learn about applications. def application do [ + mod: {ExAudit, []}, extra_applications: [:logger] ] end diff --git a/test/ex_audit_test.exs b/test/ex_audit_test.exs index 3f9e226..48ef92a 100644 --- a/test/ex_audit_test.exs +++ b/test/ex_audit_test.exs @@ -4,7 +4,7 @@ defmodule ExAuditTest do import Ecto.Query - alias ExAudit.Test.{Repo, User, Version} + alias ExAudit.Test.{Repo, User, Version, BlogPost} test "should document lifecycle of an entity" do params = %{ @@ -53,4 +53,42 @@ defmodule ExAuditTest do assert length(versions) == 3 end + + test "should track custom data" do + user = Repo.insert!(User.changeset(%User{}, %{name: "Admin", email: "admin@example.com"})) + + changeset = BlogPost.changeset(%BlogPost{}, %{ + author_id: user.id, + title: "My First Post" + }) + + {:ok, blog_post} = Repo.insert(changeset, ex_audit_custom: [actor_id: user.id]) + + version = Repo.one(from v in Version, + where: v.entity_id == ^blog_post.id, + where: v.entity_schema == ^BlogPost, + where: v.action == ^:created) + + assert version.actor_id == user.id + end + + test "should track custom data from plugs or similar" do + user = Repo.insert!(User.changeset(%User{}, %{name: "Admin", email: "admin@example.com"})) + + changeset = BlogPost.changeset(%BlogPost{}, %{ + author_id: user.id, + title: "My Second Post" + }) + + ExAudit.track(actor_id: user.id) + + {:ok, blog_post} = Repo.insert(changeset) + + version = Repo.one(from v in Version, + where: v.entity_id == ^blog_post.id, + where: v.entity_schema == ^BlogPost, + where: v.action == ^:created) + + assert version.actor_id == user.id + end end diff --git a/test/test_helper.exs b/test/test_helper.exs index 37fe83c..84b6898 100644 --- a/test/test_helper.exs +++ b/test/test_helper.exs @@ -1,3 +1,4 @@ +:ok = Application.ensure_started(:ex_audit) ExAudit.Test.Repo.start_link() Ecto.Adapters.SQL.Sandbox.mode(ExAudit.Test.Repo, :auto)