0.01
No commit activity in last 3 years
No release in over 3 years
A Sequel plugin that logs changes made to an audited model, including who created, updated and destroyed the record, and what was changed and when the change was made. This plugin provides model auditing (a.k.a: record versioning) for DB scenarios when DB triggers are not possible. (ie: on a web app on Heroku).
2005
2006
2007
2008
2009
2010
2011
2012
2013
2014
2015
2016
2017
2018
2019
2020
2021
2022
2023
2024
 Dependencies

Development

Runtime

~> 5.3
 Project Readme

Sequel::Audited

sequel-audited is a Sequel plugin that logs changes made to any audited model, including who created, updated and destroyed the record, and what was changed and when the change was made.

This plugin provides model auditing (a.k.a: record versioning) for DB scenarios when DB triggers are not possible. (ie: on a web app on Heroku).

Disclaimer

This is still work-in-progress, and is therefore NOT production ready, so use with care and test thoroughly before depending upon this gem for mission-critical stuff! You have been warned! No warranties and guarantees expressed or implied!


Having said that, the code base has 100% code coverage and I believe all significant tests pass.


Version 0.2.x

Version 0.2.x have various breaking changes and makes even more assumptions based upon my usage scenario. So your milage may vary.



Installation

1) Install the gem

Add this line to your app's Gemfile:

gem "sequel-audited"

And then execute:

$ bundle

Or install it yourself as:

$ gem install sequel-audited

2) Generate Migration

In your apps Rakefile add the following:

load "tasks/sequel-audited/migrate.rake"

Then verify that the Rake task is available by calling:

bundle exec rake -T

which should output something like this:

....
rake audited:add_migration      # Installs Sequel::Audited migration, but does not run it.
....

Run the sequel-audit rake task:

bundle exec rake audited:add_migration

After this you can comment out the rake task in your Rakefile until you need to update. And then
finally run db:migrate to update your DB.

bundle exec rake db:migrate

3) Add the :uuid plugin

You need to add the :uuid plugin to models as follows:

class YourModel < Sequel::Model
  # convert the :id primary key to a unique UUID token, for greater record security
  plugin(:uuid, field: :id)
end

...and change the primary_key :id, column to a :uuid formatted column in your model migrations.

  ...
  create_table(:your_model) do
    primary_key :id, :uuid
    ...
  end

SideNote: I'm using :uuid keys here for safer tracking of model records and to circumvent any primary key issues due to integer or string keys and wrong casting of those within the DB.


4) Add :pg_json_ops and pg_json extensions

You need to add the :pg_json extension to support JSON formatted content before instantiation the DB connection:

 # add PG JSON OPS extension. NOTE! before the DB.connect call
 Sequel.extension(:pg_json_ops)

and the :pg_json extension after the DB connection call.

 # add PG JSON extensions NOTE! after the DB.connect call.
 DB.extension :pg_json

IMPORTANT SIDENOTE!

If you are using PostgreSQL as your database, then it's a good idea to convert the the changed column to the JSON type for automatic translations into a Ruby hash.

Otherwise, you have to use JSON.parse(@v.changed) to convert it to a hash if and when you want to use it.



Usage

Using this plugin is fairly simple and straight-forward. Just add it to the individual models you wish to have audits (versions) for.

 # auditing single model
 class Post < Sequel::Model
   plugin(:audited)
 end

GOTCHA!!

Do NOT add the plugin globally to all models, as things will likely not work properly.

 # auditing all models. VERY BAD INDEED!! DO NOT DO!
 Sequel::Model.plugin :audited

By default this will audit / version all columns on the model, except the default ignored columns configured in Sequel::Audited.audited_default_ignored_columns (see Configuration Options below).


Basic usage => plugin(:audited)

 # Given a Post model with these columns: 
 [:id, :category_id, :title, :body, :author_id, :created_at, :updated_at]
 
 # Auditing all columns*
  Post.plugin :audited 
   
   #=> [:id, :category_id, :title, :body, :author_id] # audited columns
   #=> [:created_at, :updated_at]  # ignored columns
   

Advanced Usage => plugin(:audited, :only => [...])

# Auditing a Single column
  
  Post.plugin(:audited, only: :title)
    
    #=> [:title] # audited columns
    #=> [:id, :category_id, :body, :author_id, :created_at, :updated_at] # ignored columns
      
      
# Auditing Multiple columns
  
  Post.plugin(:audited, only: [:title, :body])
    #=> [:title, :body] # audited columns
    #=> [:id, :category_id, :author_id, :created_at, :updated_at] # ignored columns
    

Advanced Usage => plugin(:audited, :except => [...])

NOTE! this option does NOT ignore the default ignored columns, so use with care.

# Auditing all columns except specified columns

  Post.plugin(:audited, except: :title)
    
    #=> [:id, :category_id, :author_id, :created_at, :updated_at] # audited columns
    #=> [:title] # ignored columns
  
  
  Post.plugin(:audited, except: [:title, :author_id])
    
    #=> [:id, :category_id, :created_at, :updated_at] # audited columns
    #=> [:title, :author_id] # ignored columns



How it works

You have to look behind the curtain to see what this plugin actually does.

In a new clean DB...

1) Create

When you create a new record like this:

Category.create(name: 'Sequel')
  #<Category @values={
    :id => 1, 
    :name => "Sequel", 
    :position => 1, 
    :created_at => <timestamp>, 
    :updated_at => nil
  }>

# in the background a new row in DB[:audit_logs] has been added with the following info:

#<AuditLog @values={
  :id => 1, 
  :item_type => "Category", 
  :item_uuid => <UUID>, 
  :event => "create", 
  # NOTE! all model values are stored by default on new records.
  :changed => "{\"id\":1,\"name\":\"Sequel\",\"created_at\":\"<timestamp>\"}", 
  :version => 1, 
  :user_id => <uuid>, 
  :username => "joeblogs", 
  :user_type => "User", 
  :created_at => <timestamp>
}>

2) Updates

When you update a record like this:

cat = Category.first
cat.update(name: 'Ruby Sequel')
  #<Category @values={
    :id => 1, 
    :name => "Ruby Sequel", 
    :position => 1, 
    :created_at => <timestamp>, 
    :updated_at => <timestamp>
  }>

# in the background a new row in DB[:audit_logs] has been added with the following info:

#<AuditLog @values={
  :id => 2, 
  :item_type => "Category", 
  :item_uuid => <uuid>,
  :event => "update", 
  # NOTE! only the changes are stored
  :changed => "{\"name\":[\"Sequel\",\"Ruby Sequel\"]}", 
  :version => 2, 
  :user_id => <uuid>, 
  :username => "joeblogs", 
  :user_type => "User", 
  :created_at => <timestamp>
}>

3) Destroys (Deletes)

When you delete a record like this:

cat = Category.first
cat.delete

# in the background a new row in DB[:audit_logs] is added with the info:

#<AuditLog @values={
  :id => 3, 
  :item_type => "Category", 
  :item_uuid => <uuid>,
  :event => "destroy",
  # NOTE! all model values are stored by default on deleted records
  :changed => "{\"id\":1,\"name\":\"Ruby Sequel\",\"created_at\":\"<timestamp>\",\"updated_at\":\"<timestamp>\"}",
  :version => 3, 
  :user_id => <uuid>, 
  :username => "joeblogs", 
  :user_type => "User", 
  :created_at => <timestamp>
}>

This way you can easily track what was created, changed or deleted and who did it and when they did it.




Configuration Options

sequel-audited supports two forms of configurations:

A) Global configuration options

Sequel::Audited.audited_current_user_method

Sets the name of the global method that provides the current user object. Default is: :current_user.

You can easily change the name of this method by calling:

Sequel::Audited.audited_current_user_method = :audited_user

NOTE!

The name of the function must be given as a :symbol.

You can also customize this method on a per model basis, since the code will first try to hit the method on the model (i.e. Post) itself first. Then it will hit the global method.

Post.audited_current_user_method = :current_author

However, you are better of using the :user_method plugin configuration highlighted below.


Sequel::Audited.audited_model_name

Enables adding your own Audit model. Default is: :AuditLog

Sequel::Audited.audited_model_name = :YourCustomModel

NOTE! the name of the model must be given as a symbol.


Sequel::Audited.audited_enabled

Toggle for enabling / disabling auditing throughout all audited models. Default is: true i.e: enabled.


Sequel::Audited.audited_default_ignored_columns

An array of columns that are ignored by default. Default value is:

[:lock_version, :created_at, :updated_at, :created_on, :updated_on]

NOTE! :timestamps related columns must be ignored or you may end up with situation where an update triggers multiple copies of the record in the audit log.


# NOTE! array values must be given as symbols.
Sequel::Audited.audited_default_ignored_columns = [:id, :mycolumn, ...]

B) Per Audited Model configurations

You can also set these settings on a per model setting by passing the following options:

:user_method => :something

This option will use a different method for the current user within this model only.

Example:

# if you have a global method like 
def current_client
  @current_client ||= Client[session[:client_id]]
end

# and set 
ClientProfile.plugin(:audited, :user_method => :current_client)

# then the user info will be taken from DB[:clients].
 #<Client @values={:username=>"happyclient"... }>
 

NOTE! the current user model must respond to :id and :username attributes.


:default_ignored_columns => [...]

This option allows you to set custom default ignored columns in the audited model. It's basically just an option just-in-case, but it's probably better to use the :only => [] or :except => [] options instead (see Usage above).




Class Methods

You can easily track all changes made to a model / row / field(s) like this:

#.audited_version?

# check if model have any audits (only works on audited models)
Post.audited_versions?
  #=> returns true / false if any audits have been made

#.audited_version([conditions])

# grab all audits for a particular model. Returns an array.
Post.audited_versions
  #=> [ 
        { id: 1, item_type: 'Post', item_uuid: '<uuid>', version: 1, 
          event: 'create',  changed: "{JSON SERIALIZED OBJECT}", 
          user_id: <uuid>, username: "joeblogs", created_at: <timestamp>
        },
        {...}
       ]


# filtered by uuid key value
Posts.audited_versions(item_uuid: <uuid>)

# filtered by user :id or :username value
Posts.audited_versions(user_id: user.id)
Posts.audited_versions(username: "joeblogs")

# filtered to last two (2) days only
Posts.audited_versions(:created_at < Date.today - 2)
  1. Track all changes made by a user / user_group.
joe = User[username: "joe"]

joe.audited_versions  
  #=> returns all audits made by joe  
    ['SELECT * FROM `audit_logs` WHERE username = "joe" ORDER BY created_at DESC']

joe.audited_versions(:item_type => Post)
  #=> returns all audits made by joe on the Post model
    ['SELECT * FROM `audit_logs` WHERE username = "joe" AND item_type = 'Post' ORDER BY created_at DESC']

Instance Methods

When you call .plugin(:audited) in your model, you get these methods:

.versions

class Post < Sequel::Model
  plugin(:audited)
end

# Returns this post's versions.
post.versions  #=> [<array of versions>]

.blame

-- aliased as: .last_audited_by

# Returns the username of the user who last changed the record
post.blame
post.last_audited_by  #=> 'joeblogs'

.last_audited_at

-- aliased as: .last_audited_on

# Returns the timestamp last changed the record
post.last_audited_at
post.last_audited_on  #=> <timestamp>

Features and functionality to be implemented

Please help out if you would want this sooner than me ;-)

# Returns the post (not a version) as it looked at around the the given timestamp.
post.version_at(timestamp)

# Returns the objects (not Versions) as they were between the given times.
post.versions_between(start_time, end_time)

# Returns the post (not a version) as it was most recently.
post.previous_version

# Returns the post (not a version) as it became next.
post.next_version


# Temporarily turn Audited on for all posts.
post.audited_on!

# Temporarily turn Audited off for all posts.
post.audited_off!



TODO's

Not everything is perfect or fully formed, so this gem may be in need of the following:

  • It needs some stress testing and THREADS support & testing. Does the gem work in all situations / instances?

    I really would appreciate the wisdom of someone with a good understanding of these type of things. Please help me ensure it's working great at all times.

  • It could probably be cleaned up and made more efficient by a much better programmer than me. Please feel free to provide some suggestions or pull-requests.

  • Solid testing and support for more DB's, other than PostgreSQL currently tested against. Not a priority as I currently have no such requirements. Please feel free to submit a pull-request.

  • Testing for use with Rails, Sinatra or other Ruby frameworks. I don't see much issues here, but I'm NOT bothered to do this testing as Roda is my preferred Ruby framework. Please feel free to submit a pull-request.

  • Support for :on => [:create, :update] option to limit auditing to only some actions. Not sure if this is really worthwhile, but could be added as a feature. Please feel free to submit a pull-request.

  • Support for sweeping (compacting) old updates if there are too many. Not sure how to handle this. Suggestions and ideas are most welcome.

    I think a simple cron job could extract all records with event: 'update' older than a specific time period (3 - 6 months) and dump them into something else, instead of adding this feature.

    If you are running this on a free app on Heroku, with many and frequent updates, you might want to pay attention to this functionality as there's a 10,000 rows limit on Heroku.

Development

After checking out the repo, run bin/setup to install dependencies. Then, run rake test to run the tests. You can also run bin/console for an interactive prompt that will allow you to experiment.

To install this gem onto your local machine, run bundle exec rake install. To release a new version, update the version number in version.rb, and then run bundle exec rake release, which will create a git tag for the version, push git commits and tags, and push the .gem file to rubygems.org.

Contributing

Bug reports and pull requests are welcome on GitHub at https://github.com/kematzy/sequel-audited.

Please run bundle exec rake coverage and bundle exec rake rubocop on your code before you send a pull-request.

This project is intended to be a safe, welcoming space for collaboration, and contributors are expected to adhere to the Contributor Covenant code of conduct.

License

© Copyright Kematzy, 2015 - 2018
© Copyright Daniel Goh, 2017

Heavily inspired by:

  • the sequel gem by Jeremy Evans and many others released under the MIT license.

  • the audited gem by Brandon Keepers, Kenneth Kalmer, Daniel Morrison, Brian Ryckbost, Steve Richert & Ryan Glover released under the MIT licence.

  • the paper_trail gem by Andy Stewart & Ben Atkins released under the MIT license.

The gem is available as open source under the terms of the MIT License.