changeling 0.0.7 → 0.1.0
Sign up to get free protection for your applications and to get access to all the features.
- data/README.md +32 -22
- data/Rakefile +47 -0
- data/benchmarks/benchmark_setup.rake +16 -0
- data/benchmarks/changeling_benchmark.rake +34 -0
- data/benchmarks/tire_benchmark.rake +29 -0
- data/changeling.gemspec +3 -1
- data/lib/changeling.rb +10 -0
- data/lib/changeling/async/resque_worker.rb +11 -0
- data/lib/changeling/async/sidekiq_worker.rb +16 -0
- data/lib/changeling/async/trackling.rb +27 -0
- data/lib/changeling/exceptions/async_gem_required.rb +9 -0
- data/lib/changeling/probeling.rb +6 -3
- data/lib/changeling/trackling.rb +2 -0
- data/lib/changeling/version.rb +1 -1
- data/spec/controllers/blameling_integration_spec.rb +79 -0
- data/spec/fixtures/app/application.rb +0 -5
- data/spec/fixtures/app/models/async_blog_post.rb +10 -0
- data/spec/fixtures/app/models/async_blog_post_active_record.rb +5 -0
- data/spec/fixtures/app/models/async_blog_post_no_timestamp.rb +9 -0
- data/spec/lib/changeling/async/trackling_spec.rb +126 -0
- data/spec/lib/changeling/models/logling_spec.rb +213 -213
- data/spec/lib/changeling/probeling_spec.rb +25 -25
- data/spec/lib/changeling/trackling_spec.rb +22 -20
- data/spec/spec_helper.rb +21 -5
- metadata +139 -38
- data/spec/controllers/blameling_controller_spec.rb +0 -20
- data/spec/controllers/blog_posts_controller_spec.rb +0 -20
- data/spec/controllers/current_account_controller_spec.rb +0 -20
- data/spec/controllers/no_current_user_controller_spec.rb +0 -20
data/README.md
CHANGED
@@ -5,6 +5,8 @@
|
|
5
5
|
[travis-home]: http://travis-ci.org/
|
6
6
|
[brew-home]: http://mxcl.github.com/homebrew/
|
7
7
|
[elasticsearch-home]: http://www.elasticsearch.org
|
8
|
+
[sidekiq-home]: https://github.com/mperham/sidekiq
|
9
|
+
[resque-home]: https://github.com/defunkt/resque
|
8
10
|
|
9
11
|
A flexible and lightweight object change tracking system.
|
10
12
|
|
@@ -38,7 +40,7 @@ $ brew install elasticsearch
|
|
38
40
|
```
|
39
41
|
|
40
42
|
## Usage
|
41
|
-
|
43
|
+
### Models
|
42
44
|
Include the Trackling module for any class you want to keep track of:
|
43
45
|
|
44
46
|
```ruby
|
@@ -49,35 +51,40 @@ class Post
|
|
49
51
|
end
|
50
52
|
```
|
51
53
|
|
52
|
-
|
54
|
+
### Controllers
|
55
|
+
If you are using a Rails app, and have the notion of a "user"", you may want to track of which user made which changes.
|
56
|
+
Unfortunately models don't understand the concept of the currently signed in user. We'll have to leverage the controller to tack on this information.
|
53
57
|
|
54
58
|
```ruby
|
55
59
|
# Doesn't have to be ApplicationController, perhaps you only want it in controllers for certain resources.
|
56
60
|
class ApplicationController < ActionController::Base
|
57
61
|
include Changeling::Blameling
|
58
|
-
|
62
|
+
|
59
63
|
# Changeling assumes your user is current_user, but if not, override the changeling_blame_user method like so:
|
60
64
|
def changeling_blame_user
|
61
65
|
current_account
|
62
66
|
end
|
63
|
-
|
67
|
+
|
64
68
|
# Controller logic here...
|
65
69
|
end
|
66
70
|
```
|
67
71
|
|
68
|
-
|
69
|
-
|
72
|
+
### Asynchronous Tracking
|
73
|
+
Sometimes in high production load, you don't want another gem clogging up your resource pipeline, slowing down performance and potentially causing downtime.
|
74
|
+
Changeling has built-in Asynchronous support so you don't have to go and write your own callbacks and queues!
|
75
|
+
Changeling is compatible with [Sidekiq][sidekiq-home] and [Resque][resque-home].
|
76
|
+
When your object is saved, a job is placed in the 'changeling' queue.
|
70
77
|
|
71
78
|
```ruby
|
72
|
-
|
73
|
-
|
74
|
-
|
75
|
-
|
76
|
-
|
79
|
+
class Post
|
80
|
+
# include Changeling::Trackling
|
81
|
+
include Changeling::Async::Trackling
|
82
|
+
|
83
|
+
# Model logic here...
|
84
|
+
end
|
77
85
|
```
|
78
86
|
|
79
|
-
|
80
|
-
|
87
|
+
### Accessing Changeling's history
|
81
88
|
If you wish to see what has been logged, include the Probeling module:
|
82
89
|
|
83
90
|
```ruby
|
@@ -93,37 +100,40 @@ With Probeling, you can check out the changes that have been made! They're store
|
|
93
100
|
You can access the up to the last 10 changes simply by calling
|
94
101
|
|
95
102
|
```ruby
|
96
|
-
@post.history
|
103
|
+
# Alias for this method: @post.history
|
104
|
+
@post.loglings
|
97
105
|
```
|
98
106
|
|
99
|
-
You can access a different number of records by passing in a number to the .
|
107
|
+
You can access a different number of records by passing in a number to the .loglings method:
|
100
108
|
|
101
109
|
```ruby
|
102
110
|
# Will automatically handle if there are less than the number of histories requested.
|
103
|
-
@post.
|
111
|
+
@post.loglings(50)
|
104
112
|
```
|
105
113
|
|
106
114
|
Access all of an objects history:
|
107
115
|
|
108
116
|
```ruby
|
109
|
-
@post.all_history
|
117
|
+
# Alias for this method: @post.all_history
|
118
|
+
@post.all_loglings
|
110
119
|
```
|
111
120
|
|
112
121
|
Access all of an objects history where a specific field was changed:
|
113
122
|
|
114
123
|
```ruby
|
115
|
-
@post.history_for_field(
|
124
|
+
# Alias for this method: @post.history_for_field(field)
|
125
|
+
@post.loglings_for_field(:title)
|
116
126
|
# Or if you prefer stringified fields:
|
117
|
-
@post.
|
127
|
+
@post.loglings_for_field('title')
|
118
128
|
|
119
129
|
# You can also pass in a number to limit your results
|
120
|
-
@post.
|
130
|
+
@post.loglings_for_field(:title, 10)
|
121
131
|
```
|
122
132
|
|
123
|
-
|
133
|
+
### Logling Properties (history objects):
|
124
134
|
|
125
135
|
```ruby
|
126
|
-
log = @post.
|
136
|
+
log = @post.loglings.first
|
127
137
|
|
128
138
|
log.klass # class of the object that the Logling is tracking.
|
129
139
|
=> Post
|
data/Rakefile
CHANGED
@@ -2,6 +2,20 @@
|
|
2
2
|
require "bundler/gem_tasks"
|
3
3
|
require "rspec/core/rake_task"
|
4
4
|
|
5
|
+
Dir.glob('benchmarks/*.rake').each { |r| import r }
|
6
|
+
|
7
|
+
desc "Run all benchmarks."
|
8
|
+
task :benchmark, :count, :fields do |t, args|
|
9
|
+
args.with_defaults(:count => 1000, :fields => 20)
|
10
|
+
|
11
|
+
Rake::Task["benchmark_setup"].invoke
|
12
|
+
Rake::Task["benchmark_tire"].reenable
|
13
|
+
Rake::Task["benchmark_tire"].invoke
|
14
|
+
puts "====================================================================\n\n"
|
15
|
+
Rake::Task["benchmark_changeling"].reenable
|
16
|
+
Rake::Task["benchmark_changeling"].invoke
|
17
|
+
end
|
18
|
+
|
5
19
|
RSpec::Core::RakeTask.new("spec") do |spec|
|
6
20
|
spec.pattern = "spec/**/*_spec.rb"
|
7
21
|
end
|
@@ -12,3 +26,36 @@ RSpec::Core::RakeTask.new('spec:progress') do |spec|
|
|
12
26
|
end
|
13
27
|
|
14
28
|
task :default => :spec
|
29
|
+
|
30
|
+
|
31
|
+
# Helper methods
|
32
|
+
def generate_loglings(count, fields)
|
33
|
+
hashes = generate_logling_hashes(count, fields)
|
34
|
+
|
35
|
+
loglings = []
|
36
|
+
|
37
|
+
hashes.each { |hash| loglings << Changeling::Models::Logling.new(hash) }
|
38
|
+
|
39
|
+
loglings
|
40
|
+
end
|
41
|
+
|
42
|
+
def generate_logling_hashes(count, fields)
|
43
|
+
hashes = []
|
44
|
+
count.times do
|
45
|
+
hash = {}
|
46
|
+
|
47
|
+
hash['klass'] = "Object"
|
48
|
+
hash['oid'] = BSON::ObjectId.new.to_s
|
49
|
+
hash['modified_by'] = BSON::ObjectId.new.to_s
|
50
|
+
hash['modified_at'] = Time.now.to_s
|
51
|
+
hash['modifications'] = {}
|
52
|
+
fields.times do |time|
|
53
|
+
hash['modifications']["name_of_field_#{time}"] = ["value_of_field_before_#{time}", "value_of_field_after_#{time}"]
|
54
|
+
end
|
55
|
+
hash['modifications'] = hash['modifications'].to_json
|
56
|
+
|
57
|
+
hashes << hash
|
58
|
+
end
|
59
|
+
|
60
|
+
hashes
|
61
|
+
end
|
@@ -0,0 +1,16 @@
|
|
1
|
+
task :benchmark_setup do
|
2
|
+
Rake::Task["install"].invoke
|
3
|
+
|
4
|
+
begin
|
5
|
+
require 'changeling'
|
6
|
+
require 'bson'
|
7
|
+
rescue LoadError
|
8
|
+
require 'rubygems'
|
9
|
+
require 'changeling'
|
10
|
+
require 'bson'
|
11
|
+
end
|
12
|
+
|
13
|
+
# Setup Tire stuff for benchmarks.
|
14
|
+
Tire::Model::Search.index_prefix "changeling_benchmark"
|
15
|
+
Changeling::Models::Logling.tire.index.delete
|
16
|
+
end
|
@@ -0,0 +1,34 @@
|
|
1
|
+
desc "Benchmark Changeling processing times, from after_save hook through the final Tire index update."
|
2
|
+
task :benchmark_changeling, :count, :fields do |t, args|
|
3
|
+
args.with_defaults(:count => 1000, :fields => 20)
|
4
|
+
count = args[:count].to_i
|
5
|
+
fields = args[:fields].to_i
|
6
|
+
|
7
|
+
Rake::Task["benchmark_setup"].invoke
|
8
|
+
|
9
|
+
puts "Benchmarking Changeling: #{count} object(s) with #{fields} fields each.".upcase
|
10
|
+
puts "Generating objects. This may take a minute..."
|
11
|
+
|
12
|
+
hashes = generate_logling_hashes(count, fields)
|
13
|
+
loglings = []
|
14
|
+
|
15
|
+
puts "Done generating objects. Proceeding to benchmark..."
|
16
|
+
|
17
|
+
Benchmark.bmbm do |bm|
|
18
|
+
bm.report('Creation of Logling Index') do
|
19
|
+
Changeling::Models::Logling.tire.create_elasticsearch_index
|
20
|
+
end
|
21
|
+
|
22
|
+
bm.report("Initializing Loglings") do
|
23
|
+
hashes.each { |hash| Changeling::Models::Logling.new(hash) }
|
24
|
+
end
|
25
|
+
|
26
|
+
bm.report('Creating Loglings') do
|
27
|
+
hashes.each { |hash| Changeling::Models::Logling.create(hash) }
|
28
|
+
end
|
29
|
+
|
30
|
+
bm.report('Deletion of Logling Index') do
|
31
|
+
Changeling::Models::Logling.tire.index.delete
|
32
|
+
end
|
33
|
+
end
|
34
|
+
end
|
@@ -0,0 +1,29 @@
|
|
1
|
+
desc "Benchmark Tire indexing times alone when objects are already created."
|
2
|
+
task :benchmark_tire, :count, :fields do |t, args|
|
3
|
+
args.with_defaults(:count => 1000, :fields => 20)
|
4
|
+
count = args[:count].to_i
|
5
|
+
fields = args[:fields].to_i
|
6
|
+
|
7
|
+
Rake::Task["benchmark_setup"].invoke
|
8
|
+
|
9
|
+
puts "Benchmarking Tire: #{count} Logling(s) with #{fields} fields each.".upcase
|
10
|
+
puts "Generating loglings. This may take a minute..."
|
11
|
+
|
12
|
+
loglings = generate_loglings(count, fields)
|
13
|
+
|
14
|
+
puts "Done generating loglings. Proceeding to benchmark..."
|
15
|
+
|
16
|
+
Benchmark.bmbm do |bm|
|
17
|
+
bm.report('Creation of Logling Index') do
|
18
|
+
Changeling::Models::Logling.tire.create_elasticsearch_index
|
19
|
+
end
|
20
|
+
|
21
|
+
bm.report('Inserting Loglings into Index') do
|
22
|
+
loglings.each { |logling| logling.update_index }
|
23
|
+
end
|
24
|
+
|
25
|
+
bm.report('Deletion of Logling Index') do
|
26
|
+
Changeling::Models::Logling.tire.index.delete
|
27
|
+
end
|
28
|
+
end
|
29
|
+
end
|
data/changeling.gemspec
CHANGED
@@ -16,7 +16,7 @@ Gem::Specification.new do |gem|
|
|
16
16
|
gem.version = Changeling::VERSION
|
17
17
|
|
18
18
|
# Dependencies
|
19
|
-
gem.add_dependency "tire", "~> 0.5.
|
19
|
+
gem.add_dependency "tire", "~> 0.5.3"
|
20
20
|
gem.add_dependency "activemodel"
|
21
21
|
|
22
22
|
# Development Dependencies
|
@@ -37,4 +37,6 @@ Gem::Specification.new do |gem|
|
|
37
37
|
gem.add_development_dependency "database_cleaner"
|
38
38
|
gem.add_development_dependency "sqlite3"
|
39
39
|
gem.add_development_dependency "rails"
|
40
|
+
gem.add_development_dependency "sidekiq"
|
41
|
+
gem.add_development_dependency "resque"
|
40
42
|
end
|
data/lib/changeling.rb
CHANGED
@@ -17,6 +17,16 @@ module Changeling
|
|
17
17
|
autoload :Search, 'changeling/support/search'
|
18
18
|
end
|
19
19
|
|
20
|
+
module Async
|
21
|
+
autoload :Trackling, 'changeling/async/trackling'
|
22
|
+
autoload :SidekiqWorker, 'changeling/async/sidekiq_worker'
|
23
|
+
autoload :ResqueWorker, 'changeling/async/resque_worker'
|
24
|
+
end
|
25
|
+
|
26
|
+
module Exceptions
|
27
|
+
autoload :AsyncGemRequired, 'changeling/exceptions/async_gem_required'
|
28
|
+
end
|
29
|
+
|
20
30
|
def self.blame_user
|
21
31
|
self.changeling_store[:blame_user]
|
22
32
|
end
|
@@ -0,0 +1,16 @@
|
|
1
|
+
module Changeling
|
2
|
+
module Async
|
3
|
+
class SidekiqWorker
|
4
|
+
# If Sidekiq is not installed, we'll throw an error when trying to access Changeling::Async::Trackling
|
5
|
+
begin
|
6
|
+
include Sidekiq::Worker
|
7
|
+
sidekiq_options :queue => :changeling
|
8
|
+
rescue
|
9
|
+
end
|
10
|
+
|
11
|
+
def perform(json)
|
12
|
+
Changeling::Models::Logling.create(JSON.parse(json))
|
13
|
+
end
|
14
|
+
end
|
15
|
+
end
|
16
|
+
end
|
@@ -0,0 +1,27 @@
|
|
1
|
+
module Changeling
|
2
|
+
module Async
|
3
|
+
module Trackling
|
4
|
+
def self.included(base)
|
5
|
+
base.after_update :async_save_logling
|
6
|
+
end
|
7
|
+
|
8
|
+
def async_save_logling
|
9
|
+
unless defined?(Sidekiq) || defined?(Resque)
|
10
|
+
raise Changeling::Exceptions::AsyncGemRequired
|
11
|
+
end
|
12
|
+
|
13
|
+
if self.changes && !self.changes.empty?
|
14
|
+
logling = Changeling::Models::Logling.new(self)
|
15
|
+
|
16
|
+
if defined?(Sidekiq)
|
17
|
+
Changeling::Async::SidekiqWorker.perform_async(logling.to_indexed_json)
|
18
|
+
elsif defined?(Resque)
|
19
|
+
Resque.enqueue(Changeling::Async::ResqueWorker, logling.to_indexed_json)
|
20
|
+
end
|
21
|
+
end
|
22
|
+
|
23
|
+
true
|
24
|
+
end
|
25
|
+
end
|
26
|
+
end
|
27
|
+
end
|
@@ -0,0 +1,9 @@
|
|
1
|
+
module Changeling
|
2
|
+
module Exceptions
|
3
|
+
class AsyncGemRequired < StandardError
|
4
|
+
def to_s
|
5
|
+
"Changeling's asynchronous features require either the Sidekiq (https://github.com/mperham/sidekiq) or Resque (https://github.com/defunkt/resque) background job processing gems. Please install either one and try again."
|
6
|
+
end
|
7
|
+
end
|
8
|
+
end
|
9
|
+
end
|
data/lib/changeling/probeling.rb
CHANGED
@@ -1,15 +1,18 @@
|
|
1
1
|
module Changeling
|
2
2
|
module Probeling
|
3
|
-
def
|
3
|
+
def all_loglings
|
4
4
|
Changeling::Models::Logling.records_for(self)
|
5
5
|
end
|
6
|
+
alias_method :all_history, :all_loglings
|
6
7
|
|
7
|
-
def
|
8
|
+
def loglings(records = 10)
|
8
9
|
Changeling::Models::Logling.records_for(self, records.to_i)
|
9
10
|
end
|
11
|
+
alias_method :history, :loglings
|
10
12
|
|
11
|
-
def
|
13
|
+
def loglings_for_field(field_name, records = nil)
|
12
14
|
Changeling::Models::Logling.records_for(self, records ? records.to_i : nil, field_name.to_s)
|
13
15
|
end
|
16
|
+
alias_method :history_for_field, :loglings_for_field
|
14
17
|
end
|
15
18
|
end
|
data/lib/changeling/trackling.rb
CHANGED
data/lib/changeling/version.rb
CHANGED
@@ -0,0 +1,79 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
|
3
|
+
describe RailsApp, "Testing Blameling Integration" do
|
4
|
+
context "Controller without Blameling" do
|
5
|
+
controller(RailsApp::BlogPostsController) do
|
6
|
+
extend(RSpec::Rails::ControllerExampleGroup::BypassRescue)
|
7
|
+
end
|
8
|
+
|
9
|
+
before(:each) do
|
10
|
+
# Request needs to be setup to avoid path setting error
|
11
|
+
@request = ActionController::TestRequest.new
|
12
|
+
end
|
13
|
+
|
14
|
+
it "should not set current_user" do
|
15
|
+
Thread.new {
|
16
|
+
post :create
|
17
|
+
# Look in application.rb for the User class and it's id method.
|
18
|
+
BlogPost.last.all_loglings.last.modified_by.should == nil
|
19
|
+
}.join
|
20
|
+
end
|
21
|
+
end
|
22
|
+
|
23
|
+
context "Controller with Blameling" do
|
24
|
+
controller(RailsApp::BlamelingController) do
|
25
|
+
extend(RSpec::Rails::ControllerExampleGroup::BypassRescue)
|
26
|
+
end
|
27
|
+
|
28
|
+
before(:each) do
|
29
|
+
# Request needs to be setup to avoid path setting error
|
30
|
+
@request = ActionController::TestRequest.new
|
31
|
+
end
|
32
|
+
|
33
|
+
it "should set current_user" do
|
34
|
+
Thread.new {
|
35
|
+
post :create
|
36
|
+
# Look in application.rb for the User class and it's id method.
|
37
|
+
BlogPost.last.all_loglings.last.modified_by.should == 33
|
38
|
+
}.join
|
39
|
+
end
|
40
|
+
end
|
41
|
+
|
42
|
+
context "Controller with undefined current_user method" do
|
43
|
+
controller(RailsApp::NoCurrentUserController) do
|
44
|
+
extend(RSpec::Rails::ControllerExampleGroup::BypassRescue)
|
45
|
+
end
|
46
|
+
|
47
|
+
before(:each) do
|
48
|
+
# Request needs to be setup to avoid path setting error
|
49
|
+
@request = ActionController::TestRequest.new
|
50
|
+
end
|
51
|
+
|
52
|
+
it "should not set current_user, nor should it error out" do
|
53
|
+
Thread.new {
|
54
|
+
post :create
|
55
|
+
# Look in application.rb for the User class and it's id method.
|
56
|
+
BlogPost.last.all_loglings.last.modified_by.should == nil
|
57
|
+
}.join
|
58
|
+
end
|
59
|
+
end
|
60
|
+
|
61
|
+
context "Controller with a different overridden 'changeling_blame_user' method" do
|
62
|
+
controller(RailsApp::CurrentAccountController) do
|
63
|
+
extend(RSpec::Rails::ControllerExampleGroup::BypassRescue)
|
64
|
+
end
|
65
|
+
|
66
|
+
before(:each) do
|
67
|
+
# Request needs to be setup to avoid path setting error
|
68
|
+
@request = ActionController::TestRequest.new
|
69
|
+
end
|
70
|
+
|
71
|
+
it "should not set current_user if current_user is not defined" do
|
72
|
+
Thread.new {
|
73
|
+
post :create
|
74
|
+
# Look in application.rb for the User class and it's id method.
|
75
|
+
BlogPost.last.all_loglings.last.modified_by.should == 88
|
76
|
+
}.join
|
77
|
+
end
|
78
|
+
end
|
79
|
+
end
|