as-notifications 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
data/MIT-LICENSE ADDED
@@ -0,0 +1,20 @@
1
+ Copyright (c) 2005-2013 David Heinemeier Hansson
2
+
3
+ Permission is hereby granted, free of charge, to any person obtaining
4
+ a copy of this software and associated documentation files (the
5
+ "Software"), to deal in the Software without restriction, including
6
+ without limitation the rights to use, copy, modify, merge, publish,
7
+ distribute, sublicense, and/or sell copies of the Software, and to
8
+ permit persons to whom the Software is furnished to do so, subject to
9
+ the following conditions:
10
+
11
+ The above copyright notice and this permission notice shall be
12
+ included in all copies or substantial portions of the Software.
13
+
14
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
15
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
16
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
17
+ NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
18
+ LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
19
+ OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
20
+ WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
@@ -0,0 +1,186 @@
1
+ require 'as/notifications/instrumenter'
2
+ require 'as/notifications/fanout'
3
+
4
+ module AS
5
+ # = Notifications
6
+ #
7
+ # <tt>AS::Notifications</tt> provides an instrumentation API for
8
+ # Ruby.
9
+ #
10
+ # == Instrumenters
11
+ #
12
+ # To instrument an event you just need to do:
13
+ #
14
+ # AS::Notifications.instrument('render', extra: :information) do
15
+ # render text: 'Foo'
16
+ # end
17
+ #
18
+ # That executes the block first and notifies all subscribers once done.
19
+ #
20
+ # In the example above +render+ is the name of the event, and the rest is called
21
+ # the _payload_. The payload is a mechanism that allows instrumenters to pass
22
+ # extra information to subscribers. Payloads consist of a hash whose contents
23
+ # are arbitrary and generally depend on the event.
24
+ #
25
+ # == Subscribers
26
+ #
27
+ # You can consume those events and the information they provide by registering
28
+ # a subscriber.
29
+ #
30
+ # AS::Notifications.subscribe('render') do |name, start, finish, id, payload|
31
+ # name # => String, name of the event (such as 'render' from above)
32
+ # start # => Time, when the instrumented block started execution
33
+ # finish # => Time, when the instrumented block ended execution
34
+ # id # => String, unique ID for this notification
35
+ # payload # => Hash, the payload
36
+ # end
37
+ #
38
+ # For instance, let's store all "render" events in an array:
39
+ #
40
+ # events = []
41
+ #
42
+ # AS::Notifications.subscribe('render') do |*args|
43
+ # events << AS::Notifications::Event.new(*args)
44
+ # end
45
+ #
46
+ # That code returns right away, you are just subscribing to "render" events.
47
+ # The block is saved and will be called whenever someone instruments "render":
48
+ #
49
+ # AS::Notifications.instrument('render', extra: :information) do
50
+ # render text: 'Foo'
51
+ # end
52
+ #
53
+ # event = events.first
54
+ # event.name # => "render"
55
+ # event.duration # => 10 (in milliseconds)
56
+ # event.payload # => { extra: :information }
57
+ #
58
+ # The block in the <tt>subscribe</tt> call gets the name of the event, start
59
+ # timestamp, end timestamp, a string with a unique identifier for that event
60
+ # (something like "535801666f04d0298cd6"), and a hash with the payload, in
61
+ # that order.
62
+ #
63
+ # If an exception happens during that particular instrumentation the payload will
64
+ # have a key <tt>:exception</tt> with an array of two elements as value: a string with
65
+ # the name of the exception class, and the exception message.
66
+ #
67
+ # As the previous example depicts, the class <tt>AS::Notifications::Event</tt>
68
+ # is able to take the arguments as they come and provide an object-oriented
69
+ # interface to that data.
70
+ #
71
+ # It is also possible to pass an object as the second parameter passed to the
72
+ # <tt>subscribe</tt> method instead of a block:
73
+ #
74
+ # module ActionController
75
+ # class PageRequest
76
+ # def call(name, started, finished, unique_id, payload)
77
+ # Rails.logger.debug ['notification:', name, started, finished, unique_id, payload].join(' ')
78
+ # end
79
+ # end
80
+ # end
81
+ #
82
+ # AS::Notifications.subscribe('process_action.action_controller', ActionController::PageRequest.new)
83
+ #
84
+ # resulting in the following output within the logs including a hash with the payload:
85
+ #
86
+ # notification: process_action.action_controller 2012-04-13 01:08:35 +0300 2012-04-13 01:08:35 +0300 af358ed7fab884532ec7 {
87
+ # controller: "Devise::SessionsController",
88
+ # action: "new",
89
+ # params: {"action"=>"new", "controller"=>"devise/sessions"},
90
+ # format: :html,
91
+ # method: "GET",
92
+ # path: "/login/sign_in",
93
+ # status: 200,
94
+ # view_runtime: 279.3080806732178,
95
+ # db_runtime: 40.053
96
+ # }
97
+ #
98
+ # You can also subscribe to all events whose name matches a certain regexp:
99
+ #
100
+ # AS::Notifications.subscribe(/render/) do |*args|
101
+ # ...
102
+ # end
103
+ #
104
+ # and even pass no argument to <tt>subscribe</tt>, in which case you are subscribing
105
+ # to all events.
106
+ #
107
+ # == Temporary Subscriptions
108
+ #
109
+ # Sometimes you do not want to subscribe to an event for the entire life of
110
+ # the application. There are two ways to unsubscribe.
111
+ #
112
+ # WARNING: The instrumentation framework is designed for long-running subscribers,
113
+ # use this feature sparingly because it wipes some internal caches and that has
114
+ # a negative impact on performance.
115
+ #
116
+ # === Subscribe While a Block Runs
117
+ #
118
+ # You can subscribe to some event temporarily while some block runs. For
119
+ # example, in
120
+ #
121
+ # callback = lambda {|*args| ... }
122
+ # AS::Notifications.subscribed(callback, "sql.active_record") do
123
+ # ...
124
+ # end
125
+ #
126
+ # the callback will be called for all "sql.active_record" events instrumented
127
+ # during the execution of the block. The callback is unsubscribed automatically
128
+ # after that.
129
+ #
130
+ # === Manual Unsubscription
131
+ #
132
+ # The +subscribe+ method returns a subscriber object:
133
+ #
134
+ # subscriber = AS::Notifications.subscribe("render") do |*args|
135
+ # ...
136
+ # end
137
+ #
138
+ # To prevent that block from being called anymore, just unsubscribe passing
139
+ # that reference:
140
+ #
141
+ # AS::Notifications.unsubscribe(subscriber)
142
+ #
143
+ # == Default Queue
144
+ #
145
+ # Notifications ships with a queue implementation that consumes and publish events
146
+ # to log subscribers in a thread. You can use any queue implementation you want.
147
+ #
148
+ module Notifications
149
+ class << self
150
+ attr_accessor :notifier
151
+
152
+ def publish(name, *args)
153
+ notifier.publish(name, *args)
154
+ end
155
+
156
+ def instrument(name, payload = {})
157
+ if notifier.listening?(name)
158
+ instrumenter.instrument(name, payload) { yield payload if block_given? }
159
+ else
160
+ yield payload if block_given?
161
+ end
162
+ end
163
+
164
+ def subscribe(*args, &block)
165
+ notifier.subscribe(*args, &block)
166
+ end
167
+
168
+ def subscribed(callback, *args, &block)
169
+ subscriber = subscribe(*args, &callback)
170
+ yield
171
+ ensure
172
+ unsubscribe(subscriber)
173
+ end
174
+
175
+ def unsubscribe(args)
176
+ notifier.unsubscribe(args)
177
+ end
178
+
179
+ def instrumenter
180
+ Thread.current[:"instrumentation_#{notifier.object_id}"] ||= Instrumenter.new(notifier)
181
+ end
182
+ end
183
+
184
+ self.notifier = Fanout.new
185
+ end
186
+ end
@@ -0,0 +1,145 @@
1
+ require 'mutex_m'
2
+
3
+ module AS
4
+ module Notifications
5
+ # This is a default queue implementation that ships with Notifications.
6
+ # It just pushes events to all registered log subscribers.
7
+ #
8
+ # This class is thread safe. All methods are reentrant.
9
+ class Fanout
10
+ include Mutex_m
11
+
12
+ def initialize
13
+ @subscribers = []
14
+ @listeners_for = {}
15
+ super
16
+ end
17
+
18
+ def subscribe(pattern = nil, block = Proc.new)
19
+ subscriber = Subscribers.new pattern, block
20
+ synchronize do
21
+ @subscribers << subscriber
22
+ @listeners_for.clear
23
+ end
24
+ subscriber
25
+ end
26
+
27
+ def unsubscribe(subscriber)
28
+ synchronize do
29
+ @subscribers.reject! { |s| s.matches?(subscriber) }
30
+ @listeners_for.clear
31
+ end
32
+ end
33
+
34
+ def start(name, id, payload)
35
+ listeners_for(name).each { |s| s.start(name, id, payload) }
36
+ end
37
+
38
+ def finish(name, id, payload)
39
+ listeners_for(name).each { |s| s.finish(name, id, payload) }
40
+ end
41
+
42
+ def publish(name, *args)
43
+ listeners_for(name).each { |s| s.publish(name, *args) }
44
+ end
45
+
46
+ def listeners_for(name)
47
+ synchronize do
48
+ @listeners_for[name] ||= @subscribers.select { |s| s.subscribed_to?(name) }
49
+ end
50
+ end
51
+
52
+ def listening?(name)
53
+ listeners_for(name).any?
54
+ end
55
+
56
+ # This is a sync queue, so there is no waiting.
57
+ def wait
58
+ end
59
+
60
+ module Subscribers # :nodoc:
61
+ def self.new(pattern, listener)
62
+ if listener.respond_to?(:start) and listener.respond_to?(:finish)
63
+ subscriber = Evented.new pattern, listener
64
+ else
65
+ subscriber = Timed.new pattern, listener
66
+ end
67
+
68
+ unless pattern
69
+ AllMessages.new(subscriber)
70
+ else
71
+ subscriber
72
+ end
73
+ end
74
+
75
+ class Evented #:nodoc:
76
+ def initialize(pattern, delegate)
77
+ @pattern = pattern
78
+ @delegate = delegate
79
+ end
80
+
81
+ def start(name, id, payload)
82
+ @delegate.start name, id, payload
83
+ end
84
+
85
+ def finish(name, id, payload)
86
+ @delegate.finish name, id, payload
87
+ end
88
+
89
+ def subscribed_to?(name)
90
+ @pattern === name.to_s
91
+ end
92
+
93
+ def matches?(subscriber_or_name)
94
+ self === subscriber_or_name ||
95
+ @pattern && @pattern === subscriber_or_name
96
+ end
97
+ end
98
+
99
+ class Timed < Evented
100
+ def initialize(pattern, delegate)
101
+ @timestack = []
102
+ super
103
+ end
104
+
105
+ def publish(name, *args)
106
+ @delegate.call name, *args
107
+ end
108
+
109
+ def start(name, id, payload)
110
+ @timestack.push Time.now
111
+ end
112
+
113
+ def finish(name, id, payload)
114
+ started = @timestack.pop
115
+ @delegate.call(name, started, Time.now, id, payload)
116
+ end
117
+ end
118
+
119
+ class AllMessages # :nodoc:
120
+ def initialize(delegate)
121
+ @delegate = delegate
122
+ end
123
+
124
+ def start(name, id, payload)
125
+ @delegate.start name, id, payload
126
+ end
127
+
128
+ def finish(name, id, payload)
129
+ @delegate.finish name, id, payload
130
+ end
131
+
132
+ def publish(name, *args)
133
+ @delegate.publish name, *args
134
+ end
135
+
136
+ def subscribed_to?(name)
137
+ true
138
+ end
139
+
140
+ alias :matches? :===
141
+ end
142
+ end
143
+ end
144
+ end
145
+ end
@@ -0,0 +1,72 @@
1
+ require 'securerandom'
2
+
3
+ module AS
4
+ module Notifications
5
+ # Instrumentors are stored in a thread local.
6
+ class Instrumenter
7
+ attr_reader :id
8
+
9
+ def initialize(notifier)
10
+ @id = unique_id
11
+ @notifier = notifier
12
+ end
13
+
14
+ # Instrument the given block by measuring the time taken to execute it
15
+ # and publish it. Notice that events get sent even if an error occurs
16
+ # in the passed-in block.
17
+ def instrument(name, payload={})
18
+ start name, payload
19
+ begin
20
+ yield
21
+ rescue Exception => e
22
+ payload[:exception] = [e.class.name, e.message]
23
+ raise e
24
+ ensure
25
+ finish name, payload
26
+ end
27
+ end
28
+
29
+ # Send a start notification with +name+ and +payload+.
30
+ def start(name, payload)
31
+ @notifier.start name, @id, payload
32
+ end
33
+
34
+ # Send a finish notification with +name+ and +payload+.
35
+ def finish(name, payload)
36
+ @notifier.finish name, @id, payload
37
+ end
38
+
39
+ private
40
+
41
+ def unique_id
42
+ SecureRandom.hex(10)
43
+ end
44
+ end
45
+
46
+ class Event
47
+ attr_reader :name, :time, :transaction_id, :payload, :children
48
+ attr_accessor :end
49
+
50
+ def initialize(name, start, ending, transaction_id, payload)
51
+ @name = name
52
+ @payload = payload.dup
53
+ @time = start
54
+ @transaction_id = transaction_id
55
+ @end = ending
56
+ @children = []
57
+ end
58
+
59
+ def duration
60
+ 1000.0 * (self.end - time)
61
+ end
62
+
63
+ def <<(event)
64
+ @children << event
65
+ end
66
+
67
+ def parent_of?(event)
68
+ @children.include? event
69
+ end
70
+ end
71
+ end
72
+ end
metadata ADDED
@@ -0,0 +1,58 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: as-notifications
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.1.0
5
+ prerelease:
6
+ platform: ruby
7
+ authors:
8
+ - Bernd Ahlers
9
+ autorequire:
10
+ bindir: bin
11
+ cert_chain: []
12
+ date: 2013-01-12 00:00:00.000000000 Z
13
+ dependencies: []
14
+ description: Provides an instrumentation API for Ruby. It has been extracted from
15
+ rails activesupport.
16
+ email: bernd@tuneafish.de
17
+ executables: []
18
+ extensions: []
19
+ extra_rdoc_files: []
20
+ files:
21
+ - MIT-LICENSE
22
+ - lib/as/notifications.rb
23
+ - lib/as/notifications/fanout.rb
24
+ - lib/as/notifications/instrumenter.rb
25
+ homepage: https://github.com/bernd/as-notifications
26
+ licenses:
27
+ - MIT
28
+ post_install_message:
29
+ rdoc_options:
30
+ - --encoding
31
+ - UTF-8
32
+ require_paths:
33
+ - lib
34
+ required_ruby_version: !ruby/object:Gem::Requirement
35
+ none: false
36
+ requirements:
37
+ - - ! '>='
38
+ - !ruby/object:Gem::Version
39
+ version: '0'
40
+ segments:
41
+ - 0
42
+ hash: 948061431915797742
43
+ required_rubygems_version: !ruby/object:Gem::Requirement
44
+ none: false
45
+ requirements:
46
+ - - ! '>='
47
+ - !ruby/object:Gem::Version
48
+ version: '0'
49
+ segments:
50
+ - 0
51
+ hash: 948061431915797742
52
+ requirements: []
53
+ rubyforge_project:
54
+ rubygems_version: 1.8.24
55
+ signing_key:
56
+ specification_version: 3
57
+ summary: Provides an instrumentation API for Ruby
58
+ test_files: []