Audit logging utilities for Collection operations.
This module provides functions to log collection-related activities, errors, and track user actions for audit purposes in the Voile library management system.
Features
- Action logging with context (IP, user agent, session)
- Error logging with detailed error information
- Before/after value tracking for changes
- Performance metrics (duration tracking)
- Query helpers for retrieving audit logs
Usage Examples
Basic Action Logging
# Simple action log
CollectionLogger.log_action(collection_id, user_id, "create")
# Action with custom title and message
CollectionLogger.log_action(collection_id, user_id, "publish", [
title: "Collection Published",
message: "Collection 'My Book Collection' was published successfully"
])Advanced Action Logging with Context
# Full context logging (recommended for web requests)
CollectionLogger.log_action(collection_id, user_id, "update", [
title: "Collection Updated",
message: "Collection metadata was modified",
old_values: %{title: "Old Title", status: "draft"},
new_values: %{title: "New Title", status: "published"},
ip_address: "192.168.1.100",
user_agent: "Mozilla/5.0...",
session_id: "abc123",
request_id: "req-456",
duration_ms: 150,
metadata: %{fields_changed: ["title", "status"]}
])Error Logging
# Log validation errors
case Collection.changeset(collection, attrs) |> Repo.update() do
{:ok, collection} ->
# success handling
{:error, changeset} ->
CollectionLogger.log_error(collection.id, user_id, "update", changeset)
end
# Log custom errors
CollectionLogger.log_error(collection_id, user_id, "delete",
"Cannot delete collection with active items")Integration in Phoenix Controllers
def update(conn, %{"id" => id, "collection" => collection_params}) do
collection = Catalog.get_collection!(id)
user_id = conn.assigns.current_user.id
# Track old values before update
old_values = Map.take(collection, [:title, :status, :description])
start_time = System.monotonic_time(:millisecond)
case Catalog.update_collection(collection, collection_params) do
{:ok, updated_collection} ->
duration = System.monotonic_time(:millisecond) - start_time
new_values = Map.take(updated_collection, [:title, :status, :description])
# Log successful update
CollectionLogger.log_action(updated_collection.id, user_id, "update", [
title: "Collection Updated",
message: "Collection '#{updated_collection.title}' updated successfully",
old_values: old_values,
new_values: new_values,
duration_ms: duration,
ip_address: get_ip_address(conn),
user_agent: get_req_header(conn, "user-agent") |> List.first(),
session_id: get_session(conn, :session_id),
metadata: %{changed_fields: find_changed_fields(old_values, new_values)}
])
render(conn, :show, collection: updated_collection)
{:error, changeset} ->
# Log error
CollectionLogger.log_error(collection.id, user_id, "update", changeset, [
ip_address: get_ip_address(conn),
user_agent: get_req_header(conn, "user-agent") |> List.first()
])
render(conn, :edit, collection: collection, changeset: changeset)
end
endQuery Examples
# Get recent activity for a collection
recent_logs = CollectionLogger.recent_logs(collection_id, 20)
# Get user activity from last week
last_week = DateTime.utc_now() |> DateTime.add(-7, :day)
user_logs = CollectionLogger.user_activity(user_id, last_week)
# Get all user activity
all_user_logs = CollectionLogger.user_activity(user_id)Available Action Types
The following action types are predefined in the schema:
"create"- Collection creation"update"- Collection modification"delete"- Collection deletion"publish"- Publishing collection"unpublish"- Unpublishing collection"archive"- Archiving collection"restore"- Restoring archived collection"import"- Data import operations"export"- Data export operations
Options for log_action/4 and log_error/5
Context Options (Recommended)
:ip_address- Client IP address:user_agent- Client user agent string:session_id- User session identifier:request_id- Unique request identifier
Change Tracking Options
:old_values- Map of values before change:new_values- Map of values after change
Performance Options
:duration_ms- Operation duration in milliseconds
Custom Options
:title- Custom log title (defaults to "Collection {action}"):message- Custom log message (defaults to "Collection was {action}"):action_type- Override action type (defaults to action):metadata- Additional metadata map
Best Practices
- Always log in service/context layers, not in controllers
- Include context information (IP, user agent) for security auditing
- Track before/after values for important changes
- Use descriptive titles and messages for better audit trails
- Log both successes and failures for complete audit coverage
- Include performance metrics for operation monitoring
- Use structured metadata for easier querying and analysis
Summary
Functions
Returns the client IP address from a Plug.Conn.
Returns the user agent string from a Plug.Conn.
Logs a successful action performed on a collection.
Logs an error that occurred during a collection operation.
Retrieves recent log entries for a specific collection.
Retrieves activity logs for a specific user.
Functions
@spec get_ip_address(Plug.Conn.t()) :: String.t() | nil
Returns the client IP address from a Plug.Conn.
This checks the x-forwarded-for header first and falls back to
conn.remote_ip when the header is missing.
@spec get_user_agent(Plug.Conn.t()) :: String.t() | nil
Returns the user agent string from a Plug.Conn.
Logs a successful action performed on a collection.
Parameters
collection_id- UUID of the collectionuser_id- UUID of the user performing the actionaction- String describing the action (e.g., "create", "update", "delete")opts- Keyword list of additional options (see module documentation)
Returns
{:ok, %CollectionLog{}}- Successfully logged{:error, %Ecto.Changeset{}}- Validation or database error
Examples
# Simple logging
{:ok, log} = CollectionLogger.log_action(collection_id, user_id, "create")
# With context and change tracking
{:ok, log} = CollectionLogger.log_action(collection_id, user_id, "update", [
title: "Collection Title Updated",
message: "Changed title from 'Old' to 'New'",
old_values: %{title: "Old Title"},
new_values: %{title: "New Title"},
ip_address: "192.168.1.100",
duration_ms: 234
])
Logs an error that occurred during a collection operation.
Automatically handles different error types and formats them appropriately.
Parameters
collection_id- UUID of the collectionuser_id- UUID of the user who attempted the actionaction- String describing the attempted actionerror- The error that occurred (Changeset, Exception, or string)opts- Keyword list of additional options
Returns
{:ok, %CollectionLog{}}- Successfully logged{:error, %Ecto.Changeset{}}- Validation or database error
Examples
# Log changeset validation errors
case Repo.update(changeset) do
{:error, changeset} ->
CollectionLogger.log_error(collection_id, user_id, "update", changeset)
end
# Log custom error messages
CollectionLogger.log_error(collection_id, user_id, "delete",
"Cannot delete collection with active reservations")
# Log with context
CollectionLogger.log_error(collection_id, user_id, "publish", error, [
ip_address: "192.168.1.100",
metadata: %{attempted_status: "published"}
])
Retrieves recent log entries for a specific collection.
Returns logs ordered by most recent first, with user information preloaded.
Parameters
collection_id- UUID of the collectionlimit- Maximum number of logs to return (default: 10)
Returns
List of %CollectionLog{} structs with :user association preloaded
Examples
# Get last 10 logs
logs = CollectionLogger.recent_logs(collection_id)
# Get last 50 logs
logs = CollectionLogger.recent_logs(collection_id, 50)
# Access user information
Enum.each(logs, fn log ->
IO.puts "#{log.user.fullname} performed #{log.action} at #{log.inserted_at}"
end)
Retrieves activity logs for a specific user.
Returns logs ordered by most recent first, with collection information preloaded. Optionally filter by date range.
Parameters
user_id- UUID of the userfrom_date- Optional DateTime to filter logs from (default: all time)
Returns
List of %CollectionLog{} structs with :collection association preloaded
Examples
# Get all user activity
logs = CollectionLogger.user_activity(user_id)
# Get activity from last week
last_week = DateTime.utc_now() |> DateTime.add(-7, :day)
recent_logs = CollectionLogger.user_activity(user_id, last_week)
# Get activity from specific date
start_date = ~U[2024-01-01 00:00:00Z]
logs = CollectionLogger.user_activity(user_id, start_date)
# Access collection information
Enum.each(logs, fn log ->
collection_title = log.collection.title
IO.puts "Action: #{log.action} on '#{collection_title}'"
end)