ActivityLog / admin history

I have to build admin history similar to Django.
My goal is storage all user action. I have to know who did changes, when he did it and what he did.

I’ve already tried to save all operation in context, but this is not busines logic. So i tried put this logic inside Repo. I thought it was good idea. My Repo looks like this one

defmodule MyProject.Repo do
  use Ecto.Repo, ...
  
  def insert(changset, opts, logs_opts) do
    {status, response} = insert(changset, opts)
    if status == :ok, do: publish(changset.changes, response.uuid, logs_opts, "CREATE")
    {status, response}
  end

  def update(changset, opts, logs_opts) do
    {status, response} = update(changset, opts)
    if status == :ok, do: publish(changset.changes, response.uuid, logs_opts, "UPDATE")
    {status, response}
  end

# delete is similar

  defp publish(changes, uuid, [{:account_uuid,account_uuid}, {:module, module}], action) do
    UserActivities.create(%{
      account_uuid: account_uuid,
      action: action,
      changes: changes,
      schema: to_string(module),
      target_uuid: to_string(uuid)
    })
  end
end

But it require from me to pass account uuid inside context

  # accounts.ex
  def create(attrs, account_uuid \\ nil) do
    %Account{}
    |> Account.changeset(attrs)
    |> Repo.insert([], account_uuid: account_uuid, module: __MODULE__)
  end

I dont know how to get user fron conn inside context. I know that i can get it from controller

  def create(conn, params) do
    case Accounts.create(params, Session.get_current_user(conn)) do
      # ..
    end
  end

After few months i realized that more than 50% of my controller doesnt pass user/account to context, so my activity_log is missing.

I’ve already tried to force usage account_uuid in repo, but there is another issue. I cannot use seeds without or any script without account_uuid. I tried use "default_uid` built from “0”, but it looks bad.

So my question is “How to build activity log or logged user history”

You can check out how audit logs are implemented in hexpm and the “recently” open-sourced bytepack. In the former it’s part of Accounts context, in the latter it’s more of a separate context.

After few months i realized that more than 50% of my controller doesnt pass user/account to context, so my activity_log is missing.

I think you can just remove the default nil argument in create and you would see all the places where the current user is missing.

- def create(attrs, account_uuid \\ nil) do
+ def create(attrs, account_uuid) do

I’ve already tried to force usage account_uuid in repo, but there is another issue. I cannot use seeds without or any script without account_uuid.

So you don’t have access to account_uuids when you run the seeds but you do have access in the real app? I guess it would’ve been possible to first create seed accounts, and then use their ids in the rest of the seed functions.

What if the action is not related to Repo? For example, login, calling external api…

I had similar requirement, and put logs in the action function of controllers. So I could see whatever happened in admin controllers.

But it’s also possible to use Event Sourcing. All You need is an event store.

Do you mean Logger logs or do you also save them to DB? I’ve switched to just using plain logs recently and I wonder if it would bite me back in the future.

Logger log, but with custom logger.

something like this…

  # Log create, update, delete action
  def action(conn, _opts) do
    a_name = action_name(conn)

    if a_name in ~w(create update delete)a do
      user = conn.assigns.current_user
      message = "#{user.name}/#{user.id} #{a_name} #{current_path(conn)}"
      Logger.info(message, admin_action: true)
    end

    apply(__MODULE__, a_name, [conn, conn.params])
  end
1 Like

Not sure if we need to split the thread into a new one, but I wonder if you could tell a bit more about the custom logger (do you mean a custom backend)? How do you handle logger discarding messages, etc? With a naive setup it might be possible to overload the system and then perform some “admin” actions without them getting into logs.

In that case, I have been using LoggerFileBackend…

config :logger,
  backends: [
    ...,
    {LoggerFileBackend, :admin_action}
  ]

config :logger, :admin_action,
  path: "/path/to/log/admin_action.log",
  level: :info,
  metadata_filter: [admin_action: true]

Admin actions use their own file…

1 Like

Like i said i need to keep UserActivity. So i put any state change.

Currently there is no database INSERT in login so its not my issue. But you’re right with external api.

Still i need something that help me add information without additional code. Look django admin history.
I’m almost sure that i will forget about add log info to new schemas.