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 +20 -0
- data/lib/as/notifications.rb +186 -0
- data/lib/as/notifications/fanout.rb +145 -0
- data/lib/as/notifications/instrumenter.rb +72 -0
- metadata +58 -0
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: []
|