toystore 0.13.1 → 0.13.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (39) hide show
  1. data/Changelog.md +5 -3
  2. data/Gemfile +3 -1
  3. data/README.md +29 -1
  4. data/gemfiles/rails_3_0.gemfile +2 -0
  5. data/gemfiles/rails_3_1.gemfile +2 -0
  6. data/lib/toy.rb +10 -13
  7. data/lib/toy/callbacks.rb +1 -1
  8. data/lib/toy/dirty_store.rb +2 -2
  9. data/lib/toy/identity_map.rb +2 -2
  10. data/lib/toy/instrumentation/active_support_notifications.rb +5 -0
  11. data/lib/toy/instrumentation/log_subscriber.rb +49 -0
  12. data/lib/toy/instrumentation/metriks.rb +5 -0
  13. data/lib/toy/instrumentation/metriks_subscriber.rb +16 -0
  14. data/lib/toy/instrumentation/statsd.rb +5 -0
  15. data/lib/toy/instrumentation/statsd_subscriber.rb +22 -0
  16. data/lib/toy/instrumentation/subscriber.rb +38 -0
  17. data/lib/toy/instrumenters/memory.rb +27 -0
  18. data/lib/toy/instrumenters/noop.rb +9 -0
  19. data/lib/toy/middleware/identity_map.rb +10 -21
  20. data/lib/toy/object.rb +0 -1
  21. data/lib/toy/persistence.rb +22 -4
  22. data/lib/toy/querying.rb +46 -18
  23. data/lib/toy/version.rb +1 -1
  24. data/perf/reads.rb +1 -4
  25. data/perf/writes.rb +0 -3
  26. data/spec/helper.rb +4 -12
  27. data/spec/support/fake_udp_socket.rb +27 -0
  28. data/spec/support/instrumenter_helpers.rb +14 -0
  29. data/spec/toy/instrumentation/log_subscriber_spec.rb +85 -0
  30. data/spec/toy/instrumentation/metriks_subscriber_spec.rb +37 -0
  31. data/spec/toy/instrumentation/statsd_subscriber_spec.rb +47 -0
  32. data/spec/toy/instrumenters/memory_spec.rb +26 -0
  33. data/spec/toy/instrumenters/noop_spec.rb +22 -0
  34. data/spec/toy/persistence_spec.rb +45 -3
  35. data/spec/toy/querying_spec.rb +121 -36
  36. data/spec/toy_spec.rb +14 -16
  37. metadata +27 -7
  38. data/lib/toy/logger.rb +0 -15
  39. data/spec/toy/logger_spec.rb +0 -21
@@ -2,12 +2,14 @@
2
2
 
3
3
  I will do my best to keep this up to date with significant changes here, starting in 0.8.3.
4
4
 
5
- ## master
5
+ ## 0.13.2
6
6
 
7
- * No longer defaulting uuid to new instance, use :default instead. This allows for nil values for attributes of uuid type.
7
+ * Added instrumentation and log subscriber
8
+ * Removed Toy.logger, Toy.logger? and Toy.logger=. All logging is now through log_subscriber. See the README.
8
9
 
9
- ## 0.14.0
10
+ ## 0.13.1
10
11
 
12
+ * No longer defaulting uuid to new instance, use :default instead. This allows for nil values for attributes of uuid type.
11
13
  * No longer persisting nil attributes
12
14
  * Added Toy::Types::JSON for storing serialized JSON as an attribute value
13
15
  * Added #persisted_id and made it public so people can override confidently. It is now used in adapter.write and adapter.delete.
data/Gemfile CHANGED
@@ -1,4 +1,4 @@
1
- source :rubygems
1
+ source "https://rubygems.org"
2
2
  gemspec
3
3
 
4
4
  gem 'activesupport', '~> 3.2.0'
@@ -6,6 +6,8 @@ gem 'activemodel', '~> 3.2.0'
6
6
  gem 'rake', '~> 0.9.0'
7
7
  gem 'oj', '~> 1.0.0'
8
8
  gem 'multi_json', '~> 1.3.2'
9
+ gem 'metriks', :require => false
10
+ gem 'statsd-ruby', :require => false
9
11
 
10
12
  group(:guard) do
11
13
  gem 'guard', '~> 1.0.0'
data/README.md CHANGED
@@ -1,4 +1,4 @@
1
- # Toystore [![Build Status](https://secure.travis-ci.org/jnunemaker/toystore.png)](http://travis-ci.org/jnunemaker/toystore)
1
+ # Toystore
2
2
 
3
3
  An object mapper for any [adapter](https://github.com/jnunemaker/adapter) that can read, write, delete, and clear data.
4
4
 
@@ -206,6 +206,34 @@ If that doesn't excite you, nothing will. At this point, you are probably wishin
206
206
 
207
207
  Luckily, there is an entire directory full of [examples](https://github.com/jnunemaker/toystore/tree/master/examples) and I created a few power user guides, which I will kindly link next.
208
208
 
209
+ ## Instrumentation
210
+
211
+ ToyStore comes with a log subscriber and automatic metriks instrumentation. By
212
+ default these work with ActiveSupport::Notifications, but only require the
213
+ pieces of ActiveSupport that are needed and only do so if you actually attempt
214
+ to require the instrumentation files listed below.
215
+
216
+ To use the log subscriber:
217
+
218
+ ```ruby
219
+ # Gemfile
220
+ gem 'activesupport'
221
+
222
+ # config/initializers/toystore.rb (or wherever you want it)
223
+ require 'toy/instrumentation/log_subscriber'
224
+ ```
225
+
226
+ To use the metriks instrumentation:
227
+
228
+ ```ruby
229
+ # Gemfile
230
+ gem 'activesupport'
231
+ gem 'metriks'
232
+
233
+ # config/initializers/toystore.rb (or wherever you want it)
234
+ require 'toy/instrumentation/metriks'
235
+ ```
236
+
209
237
  ## ToyStore Power User Guides
210
238
 
211
239
  * [Wiki Home](https://github.com/jnunemaker/toystore/wiki)
@@ -6,6 +6,8 @@ gem 'activemodel', '~> 3.0.0'
6
6
  gem 'rake', '~> 0.9.0'
7
7
  gem 'oj', '~> 1.0.0'
8
8
  gem 'multi_json', '~> 1.3.2'
9
+ gem 'metriks', :require => false
10
+ gem 'statsd-ruby', :require => false
9
11
 
10
12
  group(:guard) do
11
13
  gem 'guard', '~> 1.0.0'
@@ -6,6 +6,8 @@ gem 'activemodel', '~> 3.1.0'
6
6
  gem 'rake', '~> 0.9.0'
7
7
  gem 'oj', '~> 1.0.0'
8
8
  gem 'multi_json', '~> 1.3.2'
9
+ gem 'metriks', :require => false
10
+ gem 'statsd-ruby', :require => false
9
11
 
10
12
  group(:guard) do
11
13
  gem 'guard', '~> 1.0.0'
data/lib/toy.rb CHANGED
@@ -14,21 +14,11 @@ require 'active_support/core_ext'
14
14
  extensions_path = root_path.join('lib', 'toy', 'extensions')
15
15
  Dir[extensions_path + '**/*.rb'].each { |file| require(file) }
16
16
 
17
+ require 'toy/instrumenters/noop'
18
+
17
19
  module Toy
18
20
  extend self
19
21
 
20
- def logger
21
- @logger
22
- end
23
-
24
- def logger?
25
- @logger.present?
26
- end
27
-
28
- def logger=(logger)
29
- @logger = logger
30
- end
31
-
32
22
  def key_factory=(key_factory)
33
23
  @key_factory = key_factory
34
24
  end
@@ -37,6 +27,14 @@ module Toy
37
27
  @key_factory ||= Toy::Identity::UUIDKeyFactory.new
38
28
  end
39
29
 
30
+ def instrumenter
31
+ @instrumenter || Toy::Instrumenters::Noop
32
+ end
33
+
34
+ def instrumenter=(instrumenter)
35
+ @instrumenter = instrumenter
36
+ end
37
+
40
38
  module Middleware
41
39
  autoload 'IdentityMap', 'toy/middleware/identity_map'
42
40
  end
@@ -52,7 +50,6 @@ module Toy
52
50
  autoload 'Equality', 'toy/equality'
53
51
  autoload 'Inspect', 'toy/inspect'
54
52
  autoload 'Inheritance', 'toy/inheritance'
55
- autoload 'Logger', 'toy/logger'
56
53
  autoload 'MassAssignmentSecurity', 'toy/mass_assignment_security'
57
54
  autoload 'Persistence', 'toy/persistence'
58
55
  autoload 'Querying', 'toy/querying'
@@ -7,7 +7,7 @@ module Toy
7
7
  define_model_callbacks :save, :create, :update, :destroy
8
8
  end
9
9
 
10
- def save(*)
10
+ def save(options={})
11
11
  run_callbacks(:save) { super }
12
12
  end
13
13
 
@@ -22,11 +22,11 @@ module Toy
22
22
  end
23
23
  end
24
24
 
25
- def save(*)
25
+ def save(options={})
26
26
  super.tap do
27
27
  @previously_changed = changes
28
28
  @changed_attributes.clear if @changed_attributes
29
29
  end
30
30
  end
31
31
  end
32
- end
32
+ end
@@ -62,11 +62,11 @@ module Toy
62
62
  end
63
63
  end
64
64
 
65
- def save(*)
65
+ def save(options={})
66
66
  super.tap { |result| add_to_identity_map if result }
67
67
  end
68
68
 
69
- def delete(*)
69
+ def delete
70
70
  super.tap { remove_from_identity_map }
71
71
  end
72
72
 
@@ -0,0 +1,5 @@
1
+ require 'securerandom'
2
+ require 'active_support/notifications'
3
+
4
+ # Set the instrumenter to active support notifications.
5
+ Toy.instrumenter = ActiveSupport::Notifications
@@ -0,0 +1,49 @@
1
+ require 'toy/instrumentation/active_support_notifications'
2
+ require 'active_support/log_subscriber'
3
+
4
+ module Toy
5
+ module Instrumentation
6
+ class LogSubscriber < ::ActiveSupport::LogSubscriber
7
+ def read(event)
8
+ return unless logger.debug?
9
+ log_event :read, event
10
+ end
11
+
12
+ def read_multiple(event)
13
+ return unless logger.debug?
14
+ log_event :read_multiple, event
15
+ end
16
+
17
+ def key(event)
18
+ return unless logger.debug?
19
+ log_event :key, event
20
+ end
21
+
22
+ def create(event)
23
+ return unless logger.debug?
24
+ log_event :create, event
25
+ end
26
+
27
+ def update(event)
28
+ return unless logger.debug?
29
+ log_event :update, event
30
+ end
31
+
32
+ def destroy(event)
33
+ return unless logger.debug?
34
+ log_event :destroy, event
35
+ end
36
+
37
+ def log_event(action, event)
38
+ id = event.payload[:id]
39
+ model = event.payload[:model]
40
+
41
+ name = '%s (%.1fms)' % ["#{model.name} #{action}", event.duration]
42
+
43
+ debug " #{color(name, CYAN, true)} [ #{id.inspect} ]"
44
+ end
45
+ end
46
+ end
47
+ end
48
+
49
+ Toy::Instrumentation::LogSubscriber.attach_to :toystore
@@ -0,0 +1,5 @@
1
+ require 'toy/instrumentation/active_support_notifications'
2
+ require 'toy/instrumentation/metriks_subscriber'
3
+
4
+ ActiveSupport::Notifications.subscribe /toystore/,
5
+ Toy::Instrumentation::MetriksSubscriber
@@ -0,0 +1,16 @@
1
+ # Note: You should never need to require this file directly if you are using
2
+ # ActiveSupport::Notifications. Instead, you should require the metriks file
3
+ # that lives in the same directory as this file. The benefit is that it
4
+ # subscribes to the correct events and does everything for your.
5
+ require 'toy/instrumentation/subscriber'
6
+ require 'metriks'
7
+
8
+ module Toy
9
+ module Instrumentation
10
+ class MetriksSubscriber < Subscriber
11
+ def update_timer(metric)
12
+ Metriks.timer(metric).update(@duration)
13
+ end
14
+ end
15
+ end
16
+ end
@@ -0,0 +1,5 @@
1
+ require 'toy/instrumentation/active_support_notifications'
2
+ require 'toy/instrumentation/statsd_subscriber'
3
+
4
+ ActiveSupport::Notifications.subscribe /toystore/,
5
+ Toy::Instrumentation::StatsdSubscriber
@@ -0,0 +1,22 @@
1
+ # Note: You should never need to require this file directly if you are using
2
+ # ActiveSupport::Notifications. Instead, you should require the metriks file
3
+ # that lives in the same directory as this file. The benefit is that it
4
+ # subscribes to the correct events and does everything for your.
5
+ require 'toy/instrumentation/subscriber'
6
+ require 'statsd'
7
+
8
+ module Toy
9
+ module Instrumentation
10
+ class StatsdSubscriber < Subscriber
11
+ class << self
12
+ attr_accessor :client
13
+ end
14
+
15
+ def update_timer(metric)
16
+ if self.class.client
17
+ self.class.client.timing metric, (@duration * 1_000).round
18
+ end
19
+ end
20
+ end
21
+ end
22
+ end
@@ -0,0 +1,38 @@
1
+ require 'toy/instrumentation/subscriber'
2
+
3
+ module Toy
4
+ module Instrumentation
5
+ class Subscriber
6
+ # Public: Use this as the subscribed block.
7
+ def self.call(name, start, ending, transaction_id, payload)
8
+ new(name, start, ending, transaction_id, payload).update
9
+ end
10
+
11
+ # Private: Initializes a new event processing instance.
12
+ def initialize(name, start, ending, transaction_id, payload)
13
+ @name = name
14
+ @start = start
15
+ @ending = ending
16
+ @payload = payload
17
+ @duration = ending - start
18
+ @transaction_id = transaction_id
19
+
20
+ @action = @name.split('.').first
21
+ @model = @payload[:model]
22
+ end
23
+
24
+ # Public: Actually update all the timers for the event.
25
+ #
26
+ # Returns nothing.
27
+ def update
28
+ update_timer "toystore.#{@action}"
29
+ update_timer "toystore.#{@model}.#{@action}"
30
+ end
31
+
32
+ # Internal: Override in subclass.
33
+ def update_timer(metric)
34
+ raise 'not implemented'
35
+ end
36
+ end
37
+ end
38
+ end
@@ -0,0 +1,27 @@
1
+ module Toy
2
+ module Instrumenters
3
+ # Instrumentor that is useful for tests as it stores each of the events that
4
+ # are instrumented.
5
+ class Memory
6
+ Event = Struct.new(:name, :payload, :result)
7
+
8
+ attr_reader :events
9
+
10
+ def initialize
11
+ @events = []
12
+ end
13
+
14
+ def instrument(name, payload = {})
15
+ result = if block_given?
16
+ yield payload
17
+ else
18
+ nil
19
+ end
20
+
21
+ @events << Event.new(name, payload, result)
22
+
23
+ result
24
+ end
25
+ end
26
+ end
27
+ end
@@ -0,0 +1,9 @@
1
+ module Toy
2
+ module Instrumenters
3
+ class Noop
4
+ def self.instrument(name, payload = {})
5
+ yield payload if block_given?
6
+ end
7
+ end
8
+ end
9
+ end
@@ -1,24 +1,8 @@
1
+ require 'rack/body_proxy'
2
+
1
3
  module Toy
2
4
  module Middleware
3
5
  class IdentityMap
4
- class Body
5
- def initialize(target, original)
6
- @target = target
7
- @original = original
8
- end
9
-
10
- def each(&block)
11
- @target.each(&block)
12
- end
13
-
14
- def close
15
- @target.close if @target.respond_to?(:close)
16
- ensure
17
- Toy::IdentityMap.enabled = @original
18
- Toy::IdentityMap.clear
19
- end
20
- end
21
-
22
6
  def initialize(app)
23
7
  @app = app
24
8
  end
@@ -27,9 +11,14 @@ module Toy
27
11
  Toy::IdentityMap.clear
28
12
  enabled = Toy::IdentityMap.enabled
29
13
  Toy::IdentityMap.enabled = true
30
- status, headers, body = @app.call(env)
31
- [status, headers, Body.new(body, enabled)]
14
+
15
+ response = @app.call(env)
16
+ response[2] = Rack::BodyProxy.new(response[2]) {
17
+ Toy::IdentityMap.enabled = enabled
18
+ Toy::IdentityMap.clear
19
+ }
20
+ response
32
21
  end
33
22
  end
34
23
  end
35
- end
24
+ end
@@ -10,7 +10,6 @@ module Toy
10
10
  include Dirty
11
11
  include Equality
12
12
  include Inspect
13
- include Logger
14
13
  include Inheritance
15
14
  include Serialization
16
15
 
@@ -75,8 +75,18 @@ module Toy
75
75
  !new_record? && !destroyed?
76
76
  end
77
77
 
78
- def save(*)
79
- new_record? ? create : update
78
+ def save(options={})
79
+ default_payload = {
80
+ :id => persisted_id,
81
+ :model => self.class,
82
+ }
83
+
84
+ new_record = new_record?
85
+ action = new_record ? 'create' : 'update'
86
+
87
+ Toy.instrumenter.instrument("#{action}.toystore", default_payload) { |payload|
88
+ new_record ? create : update
89
+ }
80
90
  end
81
91
 
82
92
  def update_attributes(attrs)
@@ -85,7 +95,14 @@ module Toy
85
95
  end
86
96
 
87
97
  def destroy
88
- delete
98
+ default_payload = {
99
+ :id => persisted_id,
100
+ :model => self.class,
101
+ }
102
+
103
+ Toy.instrumenter.instrument('destroy.toystore', default_payload) { |payload|
104
+ delete
105
+ }
89
106
  end
90
107
 
91
108
  def delete
@@ -112,7 +129,8 @@ module Toy
112
129
  attributes
113
130
  end
114
131
 
115
- # Public: Choke point for overriding how data gets written.
132
+ # Public: Choke point for overriding how data gets written. Don't call this
133
+ # directory, but you can safely override it.
116
134
  def persist
117
135
  adapter.write(persisted_id, persisted_attributes)
118
136
  end