/logidze

Database changes log for Rails

Primary LanguageRubyMIT LicenseMIT

Gem Version Build Status Circle CI

Logidze

Logidze provides tools for logging DB records changes. Just like audited and paper_trail do (but faster).

Logidze allows you to create a DB-level log (using triggers) and gives you an API to browse this log. The log is stored with the record itself in JSONB column. No additional tables required. Currently, only PostgreSQL 9.5+ is supported (for PostgreSQL 9.4 try jsonbx extension).

Read the story behind Logidze

Other requirements:

  • Ruby ~> 2.1;
  • Rails >= 4.2 (Yes, Rails 5 is supported!).
Sponsored by Evil Martians

Installation

  1. Add Logidze to your application's Gemfile:
gem 'logidze'
  1. Install required DB extensions and create trigger function:
rails generate logidze:install

This creates migration for adding trigger function and enabling hstore extension.

Run migrations:

rake db:migrate
  1. Add log column and triggers to the model:
rails generate logidze:model Post
rake db:migrate

This also adds has_logidze line to your model, which adds methods for working with logs.

You can provide limit option to generate to limit the size of the log (by default it's unlimited):

rails generate logidze:model Post --limit=10

To backfill table data (i.e. create initial snapshots) add backfill option:

rails generate logidze:model Post --backfill

Upgrade from previous versions

We try to make upgrade process as simple as possible. For now, the only required action is to create and run a migration:

rails generate logidze:install --update

This updates core logdize_logger DB function. No need to update tables or triggers.

Usage

Your model now has log_data column which stores changes log.

To retrieve record version at a given time use #at or #at! methods:

post = Post.find(27)

# Show current version
post.log_version #=> 3

# Show log size (number of versions)
post.log_size #=> 3

# Get copy of a record at a given time
old_post = post.at(2.days.ago)

# or revert the record itself to the previous state (without committing to DB)
post.at!('201-04-15 12:00:00')

# If no version found
post.at('1945-05-09 09:00:00') #=> nil

You can also get revision by version number:

post.at_version(2)

It is also possible to get version for relations:

Post.where(active: true).at(1.month.ago)

You can also get diff from specified time:

post.diff_from(1.hour.ago)
#=> { "id" => 27, "changes" => { "title" => { "old" => "Logidze sucks!", "new" => "Logidze rulz!" } } }

# the same for relations
Post.where(created_at: Time.zone.today.all_day).diff_from(1.hour.ago)

There are also #undo! and #redo! options (and more general #switch_to!):

# Revert record to the previous state (and stores this state in DB)
post.undo!

# You can now user redo! to revert back
post.redo!

# More generally you can revert record to arbitrary version
post.switch_to!(2)

If you update record after #undo! or #switch_to! you lose all "future" versions and #redo! is no longer possible.

Track responsibility (aka whodunnit)

You can store additional information in the version object, which is called Responsible ID. There is more likely that you would like to store the current_user.id that way.

To provide responsible_id you should wrap your code in a block:

Logidze.with_responsible(user.id) do
  post.save!
end

And then to retrieve responsible_id:

post.log_data.responsible_id

Logidze does not require responsible_id to be SomeModel ID. It can be anything. Thus Logidze does not provide methods for retrieving the corresponding object. However, you can easy write it yourself:

class Post < ActiveRecord::Base
  has_logidze

  def whodunnit
    id = log_data.responsible_id
    User.find(id) if id.present?
  end
end

And in your controller:

class ApplicationController < ActionController::Base
  around_action :set_logidze_responsible, only: [:create, :update]

  def set_logidze_responsible(&block)
    Logidze.with_responsible(current_user&.id, &block)
  end
end

Disable logging temporary

If you want to make update without logging (e.g. mass update), you can turn it off the following way:

Logidze.without_logging { Post.update_all(seen: true) }

# or

Post.without_logging { Post.update_all(seen: true) }

Log format

The log_data column has the following format:

{
  "v": 2, // current record version,
  "h": // list of changes
    [
      {
        "v": 1,  // change number
        "ts": 1460805759352, // change timestamp in milliseconds
        "c": {
            "attr": "new value",  // updated fields with new values
            "attr2": "new value"
            }
        },
        "r": 42 // Resposibility ID (if provided)
    ]
}

If you specify the limit in the trigger definition then log size will not exceed the specified size. When a new change occurs, and there is no more room for it, the two oldest changes will be merged.

Development

For development setup run ./bin/setup. This runs bundle install and creates test DB.

Contributing

Bug reports and pull requests are welcome on GitHub at https://github.com/palkan/logidze.

TODO

  • Exclude columns from the log.
  • Enhance update_all to support mass-logging.
  • Other DB adapters.

License

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