nunes 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (83) hide show
  1. data/.gitignore +17 -0
  2. data/Gemfile +11 -0
  3. data/LICENSE.txt +22 -0
  4. data/README.md +150 -0
  5. data/lib/nunes.rb +39 -0
  6. data/lib/nunes/adapter.rb +56 -0
  7. data/lib/nunes/adapters/default.rb +10 -0
  8. data/lib/nunes/adapters/memory.rb +62 -0
  9. data/lib/nunes/adapters/timing_aliased.rb +17 -0
  10. data/lib/nunes/instrumentable.rb +102 -0
  11. data/lib/nunes/subscriber.rb +69 -0
  12. data/lib/nunes/subscribers/action_controller.rb +80 -0
  13. data/lib/nunes/subscribers/action_mailer.rb +33 -0
  14. data/lib/nunes/subscribers/action_view.rb +44 -0
  15. data/lib/nunes/subscribers/active_record.rb +33 -0
  16. data/lib/nunes/subscribers/active_support.rb +58 -0
  17. data/lib/nunes/subscribers/nunes.rb +24 -0
  18. data/lib/nunes/version.rb +3 -0
  19. data/nunes.gemspec +22 -0
  20. data/script/bootstrap +21 -0
  21. data/script/test +25 -0
  22. data/script/watch +30 -0
  23. data/test/adapter_test.rb +40 -0
  24. data/test/adapters/default_test.rb +26 -0
  25. data/test/adapters/timing_aliased_test.rb +26 -0
  26. data/test/cache_instrumentation_test.rb +79 -0
  27. data/test/controller_instrumentation_test.rb +88 -0
  28. data/test/fake_udp_socket_test.rb +26 -0
  29. data/test/helper.rb +25 -0
  30. data/test/instrumentable_test.rb +100 -0
  31. data/test/mailer_instrumentation_test.rb +26 -0
  32. data/test/model_instrumentation_test.rb +55 -0
  33. data/test/nunes_test.rb +20 -0
  34. data/test/rails_app/.gitignore +15 -0
  35. data/test/rails_app/Rakefile +7 -0
  36. data/test/rails_app/app/assets/images/rails.png +0 -0
  37. data/test/rails_app/app/assets/javascripts/application.js +15 -0
  38. data/test/rails_app/app/assets/stylesheets/application.css +13 -0
  39. data/test/rails_app/app/controllers/application_controller.rb +3 -0
  40. data/test/rails_app/app/controllers/posts_controller.rb +28 -0
  41. data/test/rails_app/app/helpers/application_helper.rb +2 -0
  42. data/test/rails_app/app/mailers/.gitkeep +0 -0
  43. data/test/rails_app/app/mailers/post_mailer.rb +11 -0
  44. data/test/rails_app/app/models/.gitkeep +0 -0
  45. data/test/rails_app/app/models/post.rb +2 -0
  46. data/test/rails_app/app/views/layouts/application.html.erb +14 -0
  47. data/test/rails_app/app/views/post_mailer/created.text.erb +1 -0
  48. data/test/rails_app/app/views/posts/_post.html.erb +1 -0
  49. data/test/rails_app/app/views/posts/index.html.erb +5 -0
  50. data/test/rails_app/config.ru +4 -0
  51. data/test/rails_app/config/application.rb +67 -0
  52. data/test/rails_app/config/boot.rb +6 -0
  53. data/test/rails_app/config/database.yml +6 -0
  54. data/test/rails_app/config/environment.rb +5 -0
  55. data/test/rails_app/config/environments/development.rb +31 -0
  56. data/test/rails_app/config/environments/production.rb +64 -0
  57. data/test/rails_app/config/environments/test.rb +35 -0
  58. data/test/rails_app/config/initializers/backtrace_silencers.rb +7 -0
  59. data/test/rails_app/config/initializers/force_test_schema_load.rb +3 -0
  60. data/test/rails_app/config/initializers/inflections.rb +15 -0
  61. data/test/rails_app/config/initializers/mime_types.rb +5 -0
  62. data/test/rails_app/config/initializers/secret_token.rb +7 -0
  63. data/test/rails_app/config/initializers/session_store.rb +8 -0
  64. data/test/rails_app/config/initializers/wrap_parameters.rb +10 -0
  65. data/test/rails_app/config/locales/en.yml +5 -0
  66. data/test/rails_app/config/routes.rb +8 -0
  67. data/test/rails_app/db/migrate/20130417154459_create_posts.rb +8 -0
  68. data/test/rails_app/db/schema.rb +22 -0
  69. data/test/rails_app/db/seeds.rb +7 -0
  70. data/test/rails_app/lib/assets/.gitkeep +0 -0
  71. data/test/rails_app/lib/tasks/.gitkeep +0 -0
  72. data/test/rails_app/public/404.html +26 -0
  73. data/test/rails_app/public/422.html +26 -0
  74. data/test/rails_app/public/500.html +25 -0
  75. data/test/rails_app/public/favicon.ico +0 -0
  76. data/test/rails_app/public/index.html +241 -0
  77. data/test/rails_app/public/robots.txt +5 -0
  78. data/test/rails_app/script/rails +6 -0
  79. data/test/subscriber_test.rb +50 -0
  80. data/test/support/adapter_test_helpers.rb +33 -0
  81. data/test/support/fake_udp_socket.rb +50 -0
  82. data/test/view_instrumentation_test.rb +30 -0
  83. metadata +205 -0
data/.gitignore ADDED
@@ -0,0 +1,17 @@
1
+ *.gem
2
+ *.rbc
3
+ .bundle
4
+ .config
5
+ .yardoc
6
+ Gemfile.lock
7
+ InstalledFiles
8
+ _yardoc
9
+ coverage
10
+ doc/
11
+ lib/bundler/man
12
+ pkg
13
+ rdoc
14
+ spec/reports
15
+ test/tmp
16
+ test/version_tmp
17
+ tmp
data/Gemfile ADDED
@@ -0,0 +1,11 @@
1
+ source "https://rubygems.org"
2
+ gemspec
3
+
4
+ gem "rails", "~> 3.2.12"
5
+ gem "sqlite3-ruby", require: "sqlite3"
6
+ gem "minitest"
7
+ gem "rake"
8
+
9
+ group :watch do
10
+ gem "rb-fsevent", require: false
11
+ end
data/LICENSE.txt ADDED
@@ -0,0 +1,22 @@
1
+ Copyright (c) 2013 John Nunemaker
2
+
3
+ MIT License
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining
6
+ a copy of this software and associated documentation files (the
7
+ "Software"), to deal in the Software without restriction, including
8
+ without limitation the rights to use, copy, modify, merge, publish,
9
+ distribute, sublicense, and/or sell copies of the Software, and to
10
+ permit persons to whom the Software is furnished to do so, subject to
11
+ the following conditions:
12
+
13
+ The above copyright notice and this permission notice shall be
14
+ included in all copies or substantial portions of the Software.
15
+
16
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
17
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
18
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
19
+ NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
20
+ LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
21
+ OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
22
+ WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
data/README.md ADDED
@@ -0,0 +1,150 @@
1
+ # Nunes
2
+
3
+ The friendly gem that instruments everything for you, like I would if I could.
4
+
5
+ ## Why "nunes"?
6
+
7
+ Because I don't work for you, but even that could not stop me from trying to make it as easy as possible for you to instrument ALL THE THINGS.
8
+
9
+ ## Installation
10
+
11
+ Add this line to your application's Gemfile:
12
+
13
+ gem 'nunes'
14
+
15
+ Or install it yourself as:
16
+
17
+ $ gem install nunes
18
+
19
+ ## Usage
20
+
21
+ nunes works out of the box with statsd and instrument_agent. All you need to do is subscribe using an instance of statsd or instrumental's agent and you are good to go.
22
+
23
+ ### With Statsd
24
+
25
+ ```ruby
26
+ require 'nunes'
27
+
28
+ statsd = Statsd.new(...)
29
+ Nunes.subscribe(statsd)
30
+ ```
31
+
32
+ ### With Instrumental
33
+
34
+ ```ruby
35
+ require 'nunes'
36
+ I = Instrument::Agent.new(...)
37
+ Nunes.subscribe(I)
38
+ ```
39
+
40
+ ## What Can I Do For You?
41
+
42
+ If you are using nunes with rails, out of the box, I'll subscribe to Actve Support's notifications for:
43
+
44
+ * `process_action.action_controller`
45
+ * `render_template.action_view`
46
+ * `render_partial.action_view`
47
+ * `deliver.action_mailer`
48
+ * `receive.action_mailer`
49
+ * `sql.active_record`
50
+ * `cache_read.active_support`
51
+ * `cache_generate.active_support`
52
+ * `cache_fetch_hit.active_support`
53
+ * `cache_write.active_support`
54
+ * `cache_delete.active_support`
55
+ * `cache_exist?.active_support`
56
+
57
+ Whoa! You would do all that for me? Yep, I would. Because I care. Deeply.
58
+
59
+ Based on those events, you'll get metrics like this in statsd and instrumental:
60
+
61
+ #### Counters
62
+
63
+ * `action_controller.status.200`
64
+ * `action_controller.format.html`
65
+ * `action_controller.exception.RuntimeError` - where RuntimeError is the class of any exceptions that occur while processing a controller's action.
66
+ * `active_support.cache_hit`
67
+ * `active_support.cache_miss`
68
+
69
+ #### Timers
70
+
71
+ * `action_controller.runtime`
72
+ * `action_controller.view_runtime`
73
+ * `action_controller.db_runtime`
74
+ * `action_controller.posts.index.runtime` - where `posts` is the controller and `index` is the action
75
+ * `action_view.app.views.posts.index.html.erb` - where `app.views.posts.index.html.erb` is the path of the view file
76
+ * `action_view.app.views.posts._post.html.erb` - I can even do partials! woot woot!
77
+ * `action_mailer.deliver.post_mailer` - where `post_mailer` is the name of the mailer
78
+ * `action_mailer.receive.post_mailer` - where `post_mailer` is the name of the mailer
79
+ * `active_record.sql`
80
+ * `active_record.sql.select` - also supported are insert, update, delete, transaction_begin and transaction_commit
81
+ * `active_support.cache_read`
82
+ * `active_support.cache_generate`
83
+ * `active_support.cache_fetch`
84
+ * `active_support.cache_fetch_hit`
85
+ * `active_support.cache_write`
86
+ * `active_support.cache_delete`
87
+ * `active_support.cache_exist`
88
+
89
+ ### But wait, there's more!!!
90
+
91
+ In addition to doing all that work for you out of the box, I also allow you to wrap your own code with instrumentation. I know, I know, sounds too good to be true.
92
+
93
+ ```ruby
94
+ class User < ActiveRecord::Base
95
+ extend Nunes::Instrumentable
96
+
97
+ # wrap save and instrument the timing of it
98
+ instrument_method_time :save
99
+ end
100
+ ```
101
+
102
+ This will instrument the timing of the User instance method save. What that means is when you do this:
103
+
104
+ ```ruby
105
+ user = User.new(name: 'NUNES!')
106
+ user.save
107
+ ```
108
+
109
+ An event named `instrument_method_time.nunes` will be generated, which in turn is subscribed to and sent to whatever you used to send instrumentation to (statsd, instrumental, etc.). The metric name will default to class.method. For the example above, the metric name would be `user.save`. No fear, you can customize this.
110
+
111
+ ```ruby
112
+ class User < ActiveRecord::Base
113
+ extend Nunes::Instrumentable
114
+
115
+ # wrap save and instrument the timing of it
116
+ instrument_method_time :save, 'crazy_town.save'
117
+ end
118
+ ```
119
+
120
+ Passing a string as the second argument sets the name of the metric. You can also customize the name using a Hash as the second argument.
121
+
122
+ ```ruby
123
+ class User < ActiveRecord::Base
124
+ extend Nunes::Instrumentable
125
+
126
+ # wrap save and instrument the timing of it
127
+ instrument_method_time :save, name: 'crazy_town.save'
128
+ end
129
+ ```
130
+
131
+ In addition to name, you can also pass a payload that will get sent along with the generated event.
132
+
133
+ ```ruby
134
+ class User < ActiveRecord::Base
135
+ extend Nunes::Instrumentable
136
+
137
+ # wrap save and instrument the timing of it
138
+ instrument_method_time :save, payload: {pay: "loading"}
139
+ end
140
+ ```
141
+
142
+ If you subscribe to the event on your own, say to log some things, you'll get a key named `:pay` with a value of `"loading"` in the event's payload. Pretty neat, eh?
143
+
144
+ ## Contributing
145
+
146
+ 1. Fork it
147
+ 2. Create your feature branch (`git checkout -b my-new-feature`)
148
+ 3. Commit your changes (`git commit -am 'Add some feature'`)
149
+ 4. Push to the branch (`git push origin my-new-feature`)
150
+ 5. Create new Pull Request
data/lib/nunes.rb ADDED
@@ -0,0 +1,39 @@
1
+ require "nunes/instrumentable"
2
+
3
+ require "nunes/adapters/memory"
4
+ require "nunes/adapters/default"
5
+ require "nunes/adapters/timing_aliased"
6
+
7
+ require "nunes/subscriber"
8
+ require "nunes/subscribers/action_controller"
9
+ require "nunes/subscribers/action_view"
10
+ require "nunes/subscribers/action_mailer"
11
+ require "nunes/subscribers/active_support"
12
+ require "nunes/subscribers/active_record"
13
+ require "nunes/subscribers/nunes"
14
+
15
+ module Nunes
16
+ # Public: Shortcut method to setup all subscribers for a given client.
17
+ #
18
+ # client - The instance that will be adapted and receive all instrumentation.
19
+ #
20
+ # Examples:
21
+ #
22
+ # Nunes.subscribe(Statsd.new)
23
+ # Nunes.subscribe(Instrumental::Agent.new)
24
+ #
25
+ # Returns Array of subscribers that were setup.
26
+ def self.subscribe(client)
27
+ subscribers = []
28
+ adapter = Nunes::Adapter.wrap(client)
29
+
30
+ subscribers << Subscribers::ActionController.subscribe(adapter)
31
+ subscribers << Subscribers::ActionView.subscribe(adapter)
32
+ subscribers << Subscribers::ActionMailer.subscribe(adapter)
33
+ subscribers << Subscribers::ActiveSupport.subscribe(adapter)
34
+ subscribers << Subscribers::ActiveRecord.subscribe(adapter)
35
+ subscribers << Subscribers::Nunes.subscribe(adapter)
36
+
37
+ subscribers
38
+ end
39
+ end
@@ -0,0 +1,56 @@
1
+ module Nunes
2
+ class Adapter
3
+ # Private: Wraps a given object with the correct adapter/decorator.
4
+ #
5
+ # client - The thing to be wrapped.
6
+ #
7
+ # Returns Nunes::Adapter instance.
8
+ def self.wrap(client)
9
+ if client.nil?
10
+ raise ArgumentError.new("client cannot be nil")
11
+ end
12
+
13
+ if client.is_a?(self)
14
+ return client
15
+ end
16
+
17
+ if client.is_a?(Hash)
18
+ return Adapters::Memory.new(client)
19
+ end
20
+
21
+ has_increment = client.respond_to?(:increment)
22
+ has_timing = client.respond_to?(:timing)
23
+ has_gauge = client.respond_to?(:gauge)
24
+
25
+ if has_increment && has_timing
26
+ Adapters::Default.new(client)
27
+ elsif has_increment && has_gauge && !has_timing
28
+ Adapters::TimingAliased.new(client)
29
+ else
30
+ raise "I have no clue how to wrap what you've given me (#{client.inspect})"
31
+ end
32
+ end
33
+
34
+ # Private
35
+ attr_reader :client
36
+
37
+ # Internal: Sets the client for the adapter.
38
+ #
39
+ # client - The thing being adapted to a simple interface.
40
+ def initialize(client)
41
+ @client = client
42
+ end
43
+
44
+ # Internal: Increment a metric by a value. Override in subclass if client
45
+ # interface does not match.
46
+ def increment(metric, value = 1)
47
+ @client.increment metric, value
48
+ end
49
+
50
+ # Internal: Record a metric's duration. Override in subclass if client
51
+ # interface does not match.
52
+ def timing(metric, duration)
53
+ @client.timing metric, duration
54
+ end
55
+ end
56
+ end
@@ -0,0 +1,10 @@
1
+ require "nunes/adapter"
2
+
3
+ module Nunes
4
+ module Adapters
5
+ # Internal: Default is the assumed interface, so we don't need to override
6
+ # anything. This should never need to be used directly by a user of the gem.
7
+ class Default < ::Nunes::Adapter
8
+ end
9
+ end
10
+ end
@@ -0,0 +1,62 @@
1
+ require "nunes/adapter"
2
+
3
+ module Nunes
4
+ module Adapters
5
+ # Internal: Memory backend for recording instrumentation calls. This should
6
+ # never need to be used directly by a user of the gem.
7
+ class Memory < ::Nunes::Adapter
8
+ def initialize(client = nil)
9
+ @client = client || {}
10
+ clear
11
+ end
12
+
13
+ def increment(metric, value = 1)
14
+ counters << [metric, value]
15
+ end
16
+
17
+ def timing(metric, value)
18
+ timers << [metric, value]
19
+ end
20
+
21
+ # Internal: Returns Array of any recorded timers with durations.
22
+ def timers
23
+ @client.fetch(:timers)
24
+ end
25
+
26
+ # Internal: Returns Array of only recorded timers.
27
+ def timer_metric_names
28
+ timers.map { |op| op.first }
29
+ end
30
+
31
+ # Internal: Returns true/false if metric has been recorded as a timer.
32
+ def timer?(metric)
33
+ timers.detect { |op| op.first == metric }
34
+ end
35
+
36
+ # Internal: Returns Array of any recorded counters with values.
37
+ def counters
38
+ @client.fetch(:counters)
39
+ end
40
+
41
+ # Internal: Returns Array of only recorded counters.
42
+ def counter_metric_names
43
+ counters.map { |op| op.first }
44
+ end
45
+
46
+ # Internal: Returns true/false if metric has been recorded as a counter.
47
+ def counter?(metric)
48
+ counters.detect { |op| op.first == metric }
49
+ end
50
+
51
+ # Internal: Empties the known counters and metrics.
52
+ #
53
+ # Returns nothing.
54
+ def clear
55
+ @client ||= {}
56
+ @client.clear
57
+ @client[:timers] = []
58
+ @client[:counters] = []
59
+ end
60
+ end
61
+ end
62
+ end
@@ -0,0 +1,17 @@
1
+ require "nunes/adapter"
2
+
3
+ module Nunes
4
+ module Adapters
5
+ # Internal: Adapter that aliases timing to gauge. One of the supported
6
+ # places to send instrumentation data is instrumentalapp.com. Their agent
7
+ # uses gauge under the hood for timing information. This adapter is used to
8
+ # adapter their gauge interface to the timing one used internally in the
9
+ # gem. This should never need to be used directly by a user of the gem.
10
+ class TimingAliased < ::Nunes::Adapter
11
+ # Internal: Adapter timing to gauge.
12
+ def timing(metric, duration)
13
+ @client.gauge metric, duration
14
+ end
15
+ end
16
+ end
17
+ end
@@ -0,0 +1,102 @@
1
+ require "active_support/notifications"
2
+
3
+ module Nunes
4
+ # Extend and instrument. Simple class that makes it easy to instrument method
5
+ # timing using ActiveSupport::Notifications.
6
+ #
7
+ # The event name is the name of the method being instrumented and the event
8
+ # namespace is the name of the class the method is in.
9
+ #
10
+ # Examples
11
+ #
12
+ # # To instrument an instance method, extend the module and instrument it.
13
+ # class User
14
+ # # Only need to do this once.
15
+ # extend Nunes::Instrumentable
16
+ #
17
+ # def something
18
+ # # ...
19
+ # end
20
+ #
21
+ # instrument_method_time :something
22
+ #
23
+ # # you can customize the event and namespace by providing the name option
24
+ # instrument_method_time :something, {
25
+ # name: "something.else.User",
26
+ # }
27
+ #
28
+ # # you can also add additional payload items
29
+ # instrument_method_time :something, {
30
+ # payload: {some: 'thing'},
31
+ # }
32
+ # end
33
+ #
34
+ # # To instrument a class method, you need to extend the module on the
35
+ # # singleton class and use the same to call the method.
36
+ # class User
37
+ # # Only need to do this once.
38
+ # singleton_class.extend Nunes::Instrumentable
39
+ #
40
+ # def self.something_class_level
41
+ # # ...
42
+ # end
43
+ #
44
+ # singleton_class.instrument_method_time :someting_class_level
45
+ # end
46
+ #
47
+ module Instrumentable
48
+ # Private
49
+ MethodTimeEventName = "instrument_method_time.nunes".freeze
50
+
51
+ # Public: Instrument a method's timing by name.
52
+ #
53
+ # method_name - The String or Symbol name of the method.
54
+ # options_or_string - The Hash of options or the String metic name.
55
+ # :payload - Any items you would like to include with the
56
+ # instrumentation payload.
57
+ # :name - The String name of the event and namespace.
58
+ def instrument_method_time(method_name, options_or_string = nil)
59
+ options = options_or_string || {}
60
+ options = {name: options} if options.is_a?(String)
61
+
62
+ action = :time
63
+ payload = options.fetch(:payload) { {} }
64
+ instrumenter = options.fetch(:instrumenter) { ActiveSupport::Notifications }
65
+
66
+ payload[:metric] = options.fetch(:name) {
67
+ "#{self.name}/#{method_name}"
68
+ }.to_s.underscore.gsub('/', '.')
69
+
70
+ nunes_wrap_method(method_name, action) do |old_method_name, new_method_name|
71
+ define_method(new_method_name) do |*args, &block|
72
+ instrumenter.instrument(MethodTimeEventName, payload) {
73
+ result = send(old_method_name, *args, &block)
74
+
75
+ payload[:arguments] = args
76
+ payload[:result] = result
77
+
78
+ result
79
+ }
80
+ end
81
+ end
82
+ end
83
+
84
+ # Private: And so horrendously ugly...
85
+ def nunes_wrap_method(method_name, action, &block)
86
+ method_without_instrumentation = :"#{method_name}_without_#{action}"
87
+ method_with_instrumentation = :"#{method_name}_with_#{action}"
88
+
89
+ if method_defined?(method_without_instrumentation)
90
+ raise ArgumentError, "already instrumented #{method_name} for #{self.name}"
91
+ end
92
+
93
+ if !method_defined?(method_name) && !private_method_defined?(method_name)
94
+ raise ArgumentError, "could not find method #{method_name} for #{self.name}"
95
+ end
96
+
97
+ alias_method method_without_instrumentation, method_name
98
+ yield method_without_instrumentation, method_with_instrumentation
99
+ alias_method method_name, method_with_instrumentation
100
+ end
101
+ end
102
+ end