as-notifications 0.1.0

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.
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: []