Audited
Audited is one of the most popular gems in Ruby on Rails. This gem will help us keep track of what/when/who changed a record.
In this article, we will install audited and discover how it works in our application.
Table of Contents
What is Audited
Audited (formerly known as acts_as_audited) is an ORM plugin that records all model modifications. Audited additionally keeps track of who made the modifications, saves comments, and associates models with the changes.
How to install audited
Reminder:
Make sure that your containers are up and running.
In your Gemfile, add gem audited
.
gem 'audited'
Then run bundle install.
root@0122:/usr/src/app# bundle install
Fetching gem metadata from https://rubygems.org/..........
Resolving dependencies...
...
Installing audited 5.2.0
Bundle complete! 24 Gemfile dependencies, 96 gems now installed.
Use `bundle info [gemname]` to see where a bundled gem is installed.
Setup
Create the audits
table with rails generate audited:install
command, then migrate.
root@0122:/usr/src/app# rails generate audited:install
create db/migrate/xxxxxxxxxxxxxx_install_audited.rb
root@0122:/usr/src/app# rails db:migrate
== xxxxxxxxxxxxxx InstallAudited: migrating ===================================
-- create_table(:audits, {:force=>true}) -> 0.0284s
-- add_index(:audits, [:auditable_type, :auditable_id, :version], {:name=>"auditable_index"}) -> 0.0134s
-- add_index(:audits, [:associated_type, :associated_id], {:name=>"associated_index"}) -> 0.0163s
-- add_index(:audits, [:user_id, :user_type], {:name=>"user_index"}) -> 0.0120s
-- add_index(:audits, :request_uuid) -> 0.0120s
-- add_index(:audits, :created_at) -> 0.0116s
== xxxxxxxxxxxxxx InstallAudited: migrated (0.0956s) ==========================
By default, the “safe YAML” loading method does not enable all classes to be deserialized (rails 6+ versions
). To identify classes in our application that are judged as safe, we need to add it to yaml column permitted classes in our application.rb
.
# config/application.rb
# ...
module App
class Application < Rails::Application
# ...
config.generators.system_tests = nil
+ config.active_record.yaml_column_permitted_classes = %w[String Integer NilClass Float Time Date FalseClass Hash Array DateTime TrueClass BigDecimal
+ ActiveSupport::TimeWithZone ActiveSupport::TimeZone ActiveSupport::HashWithIndifferentAccess]
end
end
Usage
Simply call audited
on the model you want to audit.
# app/models/order.rb
class Order < ApplicationRecord
include AASM
+ audited
belongs_to :user
# ...
end
Try it out on console and see the audits.
irb(main):001:0> user = User.client.first
User Load (0.4ms) SELECT `users`.* FROM `users` WHERE `users`.`genre` = 0 ORDER BY `users`.`id` ASC LIMIT 1
=> #<User id: 2, email: "sid.klocko@bednar-koss.net", created_at: "xxxx-xx-xx xx:xx:xx.xxxxxxxxx +xxxx", updated_at: "xxxx-xx-xx xx:xx:xx.xxxxxxxxx +xxxx", genre: "client", balance: 0.5e2>
irb(main):002:0> order = user.orders.create(amount: 20)
TRANSACTION (0.2ms) BEGIN
Order Create (0.2ms) INSERT INTO `orders` (`amount`, `serial_number`, `user_id`, `state`, `created_at`, `updated_at`) VALUES (20.0, NULL, 2, 'pending', 'xxxx-xx-xx xx:xx:xx.xxxxxxxxx', 'xxxx-xx-xx xx:xx:xx.xxxxxxxxx')
Audited::Audit Create (0.4ms) INSERT INTO `audits` (`auditable_id`, `auditable_type`, `associated_id`, `associated_type`, `user_id`, `user_type`, `username`, `action`, `audited_changes`, `version`, `comment`, `remote_address`, `request_uuid`, `created_at`) VALUES (19, 'Order', NULL, NULL, NULL, NULL, NULL, 'create', '---\namount: !ruby/object:BigDecimal 18:0.2e2\nserial_number: \nuser_id: 2\nstate: pending\n', 1, NULL, NULL, 'f825fd62-b4b6-40f0-9411-82fd4a8a781a', 'xxxx-xx-xx xx:xx:xx.xxxxxxxxx')
Audited::Audit Maximum (0.5ms) SELECT MAX(`audits`.`version`) FROM `audits` WHERE `audits`.`auditable_id` = 19 AND `audits`.`auditable_type` = 'Order'
Audited::Audit Create (0.4ms) INSERT INTO `audits` (`auditable_id`, `auditable_type`, `associated_id`, `associated_type`, `user_id`, `user_type`, `username`, `action`, `audited_changes`, `version`, `comment`, `remote_address`, `request_uuid`, `created_at`) VALUES (19, 'Order', NULL, NULL, NULL, NULL, NULL, 'update', '---\nserial_number:\n- \n- gem-000000019\n', 2, NULL, NULL, '700068a5-7db1-47d4-85df-6cd52641b9f0', 'xxxx-xx-xx xx:xx:xx.xxxxxxxxx')
Order Update (0.4ms) UPDATE `orders` SET `orders`.`serial_number` = 'gem-000000019', `orders`.`updated_at` = 'xxxx-xx-xx xx:xx:xx.xxxxxxxxx' WHERE `orders`.`id` = 19
TRANSACTION (0.6ms) COMMIT
=> #<Order:0x00007f8e509067b8>
irb(main):0043:0> order.audits
Audited::Audit Load (0.5ms) SELECT `audits`.* FROM `audits` WHERE `audits`.`auditable_id` = 19 AND `audits`.`auditable_type` = 'Order' ORDER BY `audits`.`version` ASC
=>
[#<Audited::Audit:0x00007f8e50685958
id: 1, auditable_id: 19, auditable_type: "Order",
...
action: "create",
audited_changes: {"amount"=>0.2e2, "serial_number"=>nil, "user_id"=>2, "state"=>"pending"},
... >,
#<Audited::Audit:0x00005591c1bde018
id: 1, auditable_id: 19, auditable_type: "Order",
...
action: "update",
audited_changes: {"serial_number"=>[nil, "gem-000000019"]},
... >]
Current User Tracking
All audited modifications made in a request will automatically be attributed to the current user. Audited utilizes the current_user
function in your controller by default.
Let’s create a TopUps
feature that will create order. Add resources top_ups
for new
and create
actions.
# config/routes.rb
Rails.application.routes.draw do
# ...
constraints(ClientDomainConstraint.new) do
resources :posts do
resources :comments, except: :show
end
+ resources :top_ups, only: [:new, :create]
end
# ...
end
Generate controller.
root@0122:/usr/src/app# rails g controller TopUps
create app/controllers/top_ups_controller.rb
invoke erb
create app/views/top_ups
invoke helper
create app/helpers/top_ups_helper.rb
Modify the TopUpsController
.
# app/controllers/top_ups_controller.rb
class TopUpsController < ApplicationController
before_action :authenticate_user!
def new
@order = Order.new
end
def create
@order = Order.new
@order.amount = params[:order][:amount]
@order.user = current_user
@order.save
end
end
Add new top ups view.
<!-- app/views/top_ups/new.html.erb -->
<%= form_with model: @order, url: top_ups_path do |f| %>
<%= f.label :amount %>
<%= f.number_field :amount %>
<%= f.submit %>
<% end %>
Temporarily add debugger
to create and take a look at what is happening.
# app/controllers/top_ups_controller.rb
class TopUpsController < ApplicationController
# ...
def create
@order = Order.new
@order.amount = params[:order][:amount]
@order.user = current_user
@order.save
+ debugger
end
end
Login a client user, go to client.com:3000/top_ups/new
, enter an amount and submit. Now take a look at logs.
xx:xx:xx web.1 | 8| def create
xx:xx:xx web.1 | 9| @order = Order.new
xx:xx:xx web.1 | 10| @order.amount = params[:order][:amount]
xx:xx:xx web.1 | 11| @order.user = current_user
xx:xx:xx web.1 | 12| @order.save
xx:xx:xx web.1 | => 13| debugger
xx:xx:xx web.1 | 14| end
xx:xx:xx web.1 | 15| end
xx:xx:xx web.1 | (rdbg) current_user
xx:xx:xx web.1 | #<User id: 2, email: "sid.klocko@bednar-koss.net", ... >
xx:xx:xx web.1 | (rdbg) @order.audits.last.user
xx:xx:xx web.1 | #<User id: 2, email: "sid.klocko@bednar-koss.net", ... >
Remove the debugger.
# app/controllers/top_ups_controller.rb
class TopUpsController < ApplicationController
# ...
def create
@order = Order.new
@order.amount = params[:order][:amount]
@order.user = current_user
@order.save
- debugger
end
end
Now we successfully integrated audited
Gem into our application. To know more about audited Gem, read Ruby Docs Audited.