skylight 0.0.2
Sign up to get free protection for your applications and to get access to all the features.
- data/README.md +20 -0
- data/lib/skylight.rb +33 -0
- data/lib/skylight/compat.rb +9 -0
- data/lib/skylight/compat/notifications.rb +175 -0
- data/lib/skylight/compat/notifications/fanout.rb +166 -0
- data/lib/skylight/compat/notifications/instrumenter.rb +65 -0
- data/lib/skylight/config.rb +82 -0
- data/lib/skylight/connection.rb +25 -0
- data/lib/skylight/instrumenter.rb +106 -0
- data/lib/skylight/json_proto.rb +82 -0
- data/lib/skylight/middleware.rb +23 -0
- data/lib/skylight/railtie.rb +67 -0
- data/lib/skylight/subscriber.rb +37 -0
- data/lib/skylight/trace.rb +109 -0
- data/lib/skylight/util/atomic.rb +73 -0
- data/lib/skylight/util/bytes.rb +40 -0
- data/lib/skylight/util/clock.rb +37 -0
- data/lib/skylight/util/ewma.rb +32 -0
- data/lib/skylight/util/gzip.rb +15 -0
- data/lib/skylight/util/queue.rb +93 -0
- data/lib/skylight/util/uniform_sample.rb +63 -0
- data/lib/skylight/util/uuid.rb +33 -0
- data/lib/skylight/version.rb +3 -0
- data/lib/skylight/worker.rb +232 -0
- metadata +85 -0
data/README.md
ADDED
@@ -0,0 +1,20 @@
|
|
1
|
+
# Direwolf Agent
|
2
|
+
|
3
|
+
Collect instrumentation from a rails application and send it off to the
|
4
|
+
direwolf servers.
|
5
|
+
|
6
|
+
## Traces
|
7
|
+
|
8
|
+
An conceptual overview of a trace.
|
9
|
+
|
10
|
+
* Trace ID (UUID)
|
11
|
+
* Tiers
|
12
|
+
* Name
|
13
|
+
* Spans (max 256 unique per tier)
|
14
|
+
* Category (multi-level, example: cache.redis)
|
15
|
+
* Description
|
16
|
+
* Annotations
|
17
|
+
|
18
|
+
## TODO
|
19
|
+
|
20
|
+
* Cap the max number of spans per trace (2048k?)
|
data/lib/skylight.rb
ADDED
@@ -0,0 +1,33 @@
|
|
1
|
+
require 'thread'
|
2
|
+
require 'socket'
|
3
|
+
require 'openssl'
|
4
|
+
require 'net/http'
|
5
|
+
require 'active_support/notifications'
|
6
|
+
|
7
|
+
module Skylight
|
8
|
+
class Error < RuntimeError; end
|
9
|
+
|
10
|
+
# First require all util files
|
11
|
+
require 'skylight/util/atomic'
|
12
|
+
require 'skylight/util/bytes'
|
13
|
+
require 'skylight/util/clock'
|
14
|
+
require 'skylight/util/ewma'
|
15
|
+
require 'skylight/util/gzip'
|
16
|
+
require 'skylight/util/queue'
|
17
|
+
require 'skylight/util/uniform_sample'
|
18
|
+
require 'skylight/util/uuid'
|
19
|
+
|
20
|
+
# Then require the rest
|
21
|
+
require 'skylight/compat'
|
22
|
+
require 'skylight/config'
|
23
|
+
require 'skylight/instrumenter'
|
24
|
+
require 'skylight/middleware'
|
25
|
+
require 'skylight/json_proto'
|
26
|
+
require 'skylight/subscriber'
|
27
|
+
require 'skylight/trace'
|
28
|
+
require 'skylight/worker'
|
29
|
+
end
|
30
|
+
|
31
|
+
if defined?(Rails)
|
32
|
+
require 'skylight/railtie'
|
33
|
+
end
|
@@ -0,0 +1,175 @@
|
|
1
|
+
require 'skylight/compat/notifications/instrumenter'
|
2
|
+
require 'skylight/compat/notifications/fanout'
|
3
|
+
|
4
|
+
module ActiveSupport
|
5
|
+
# = Notifications
|
6
|
+
#
|
7
|
+
# <tt>ActiveSupport::Notifications</tt> provides an instrumentation API for Ruby.
|
8
|
+
#
|
9
|
+
# == Instrumenters
|
10
|
+
#
|
11
|
+
# To instrument an event you just need to do:
|
12
|
+
#
|
13
|
+
# ActiveSupport::Notifications.instrument("render", :extra => :information) do
|
14
|
+
# render :text => "Foo"
|
15
|
+
# end
|
16
|
+
#
|
17
|
+
# That executes the block first and notifies all subscribers once done.
|
18
|
+
#
|
19
|
+
# In the example above "render" is the name of the event, and the rest is called
|
20
|
+
# the _payload_. The payload is a mechanism that allows instrumenters to pass
|
21
|
+
# extra information to subscribers. Payloads consist of a hash whose contents
|
22
|
+
# are arbitrary and generally depend on the event.
|
23
|
+
#
|
24
|
+
# == Subscribers
|
25
|
+
#
|
26
|
+
# You can consume those events and the information they provide by registering
|
27
|
+
# a subscriber. For instance, let's store all "render" events in an array:
|
28
|
+
#
|
29
|
+
# events = []
|
30
|
+
#
|
31
|
+
# ActiveSupport::Notifications.subscribe("render") do |*args|
|
32
|
+
# events << ActiveSupport::Notifications::Event.new(*args)
|
33
|
+
# end
|
34
|
+
#
|
35
|
+
# That code returns right away, you are just subscribing to "render" events.
|
36
|
+
# The block is saved and will be called whenever someone instruments "render":
|
37
|
+
#
|
38
|
+
# ActiveSupport::Notifications.instrument("render", :extra => :information) do
|
39
|
+
# render :text => "Foo"
|
40
|
+
# end
|
41
|
+
#
|
42
|
+
# event = events.first
|
43
|
+
# event.name # => "render"
|
44
|
+
# event.duration # => 10 (in milliseconds)
|
45
|
+
# event.payload # => { :extra => :information }
|
46
|
+
#
|
47
|
+
# The block in the <tt>subscribe</tt> call gets the name of the event, start
|
48
|
+
# timestamp, end timestamp, a string with a unique identifier for that event
|
49
|
+
# (something like "535801666f04d0298cd6"), and a hash with the payload, in
|
50
|
+
# that order.
|
51
|
+
#
|
52
|
+
# If an exception happens during that particular instrumentation the payload will
|
53
|
+
# have a key <tt>:exception</tt> with an array of two elements as value: a string with
|
54
|
+
# the name of the exception class, and the exception message.
|
55
|
+
#
|
56
|
+
# As the previous example depicts, the class <tt>ActiveSupport::Notifications::Event</tt>
|
57
|
+
# is able to take the arguments as they come and provide an object-oriented
|
58
|
+
# interface to that data.
|
59
|
+
#
|
60
|
+
# It is also possible to pass an object as the second parameter passed to the
|
61
|
+
# <tt>subscribe</tt> method instead of a block:
|
62
|
+
#
|
63
|
+
# module ActionController
|
64
|
+
# class PageRequest
|
65
|
+
# def call(name, started, finished, unique_id, payload)
|
66
|
+
# Rails.logger.debug ["notification:", name, started, finished, unique_id, payload].join(" ")
|
67
|
+
# end
|
68
|
+
# end
|
69
|
+
# end
|
70
|
+
#
|
71
|
+
# ActiveSupport::Notifications.subscribe('process_action.action_controller', ActionController::PageRequest.new)
|
72
|
+
#
|
73
|
+
# resulting in the following output within the logs including a hash with the payload:
|
74
|
+
#
|
75
|
+
# notification: process_action.action_controller 2012-04-13 01:08:35 +0300 2012-04-13 01:08:35 +0300 af358ed7fab884532ec7 {
|
76
|
+
# :controller=>"Devise::SessionsController",
|
77
|
+
# :action=>"new",
|
78
|
+
# :params=>{"action"=>"new", "controller"=>"devise/sessions"},
|
79
|
+
# :format=>:html,
|
80
|
+
# :method=>"GET",
|
81
|
+
# :path=>"/login/sign_in",
|
82
|
+
# :status=>200,
|
83
|
+
# :view_runtime=>279.3080806732178,
|
84
|
+
# :db_runtime=>40.053
|
85
|
+
# }
|
86
|
+
#
|
87
|
+
# You can also subscribe to all events whose name matches a certain regexp:
|
88
|
+
#
|
89
|
+
# ActiveSupport::Notifications.subscribe(/render/) do |*args|
|
90
|
+
# ...
|
91
|
+
# end
|
92
|
+
#
|
93
|
+
# and even pass no argument to <tt>subscribe</tt>, in which case you are subscribing
|
94
|
+
# to all events.
|
95
|
+
#
|
96
|
+
# == Temporary Subscriptions
|
97
|
+
#
|
98
|
+
# Sometimes you do not want to subscribe to an event for the entire life of
|
99
|
+
# the application. There are two ways to unsubscribe.
|
100
|
+
#
|
101
|
+
# WARNING: The instrumentation framework is designed for long-running subscribers,
|
102
|
+
# use this feature sparingly because it wipes some internal caches and that has
|
103
|
+
# a negative impact on performance.
|
104
|
+
#
|
105
|
+
# === Subscribe While a Block Runs
|
106
|
+
#
|
107
|
+
# You can subscribe to some event temporarily while some block runs. For
|
108
|
+
# example, in
|
109
|
+
#
|
110
|
+
# callback = lambda {|*args| ... }
|
111
|
+
# ActiveSupport::Notifications.subscribed(callback, "sql.active_record") do
|
112
|
+
# ...
|
113
|
+
# end
|
114
|
+
#
|
115
|
+
# the callback will be called for all "sql.active_record" events instrumented
|
116
|
+
# during the execution of the block. The callback is unsubscribed automatically
|
117
|
+
# after that.
|
118
|
+
#
|
119
|
+
# === Manual Unsubscription
|
120
|
+
#
|
121
|
+
# The +subscribe+ method returns a subscriber object:
|
122
|
+
#
|
123
|
+
# subscriber = ActiveSupport::Notifications.subscribe("render") do |*args|
|
124
|
+
# ...
|
125
|
+
# end
|
126
|
+
#
|
127
|
+
# To prevent that block from being called anymore, just unsubscribe passing
|
128
|
+
# that reference:
|
129
|
+
#
|
130
|
+
# ActiveSupport::Notifications.unsubscribe(subscriber)
|
131
|
+
#
|
132
|
+
# == Default Queue
|
133
|
+
#
|
134
|
+
# Notifications ships with a queue implementation that consumes and publish events
|
135
|
+
# to log subscribers in a thread. You can use any queue implementation you want.
|
136
|
+
#
|
137
|
+
module Notifications
|
138
|
+
class << self
|
139
|
+
attr_accessor :notifier
|
140
|
+
|
141
|
+
def publish(name, *args)
|
142
|
+
notifier.publish(name, *args)
|
143
|
+
end
|
144
|
+
|
145
|
+
def instrument(name, payload = {}, &blk)
|
146
|
+
if notifier.listening?(name)
|
147
|
+
instrumenter.instrument(name, payload, &blk)
|
148
|
+
else
|
149
|
+
yield payload if block_given?
|
150
|
+
end
|
151
|
+
end
|
152
|
+
|
153
|
+
def subscribe(*args, &block)
|
154
|
+
notifier.subscribe(*args, &block)
|
155
|
+
end
|
156
|
+
|
157
|
+
def subscribed(callback, *args, &block)
|
158
|
+
subscriber = subscribe(*args, &callback)
|
159
|
+
yield
|
160
|
+
ensure
|
161
|
+
unsubscribe(subscriber)
|
162
|
+
end
|
163
|
+
|
164
|
+
def unsubscribe(args)
|
165
|
+
notifier.unsubscribe(args)
|
166
|
+
end
|
167
|
+
|
168
|
+
def instrumenter
|
169
|
+
Thread.current[:"instrumentation_#{notifier.object_id}"] ||= Instrumenter.new(notifier)
|
170
|
+
end
|
171
|
+
end
|
172
|
+
|
173
|
+
self.notifier = Fanout.new
|
174
|
+
end
|
175
|
+
end
|
@@ -0,0 +1,166 @@
|
|
1
|
+
require 'mutex_m'
|
2
|
+
|
3
|
+
module ActiveSupport
|
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 measure(name, id, payload)
|
43
|
+
listeners_for(name).each { |s| s.measure(name, id, payload) }
|
44
|
+
end
|
45
|
+
|
46
|
+
def publish(name, *args)
|
47
|
+
listeners_for(name).each { |s| s.publish(name, *args) }
|
48
|
+
end
|
49
|
+
|
50
|
+
def listeners_for(name)
|
51
|
+
synchronize do
|
52
|
+
@listeners_for[name] ||= @subscribers.select { |s| s.subscribed_to?(name) }
|
53
|
+
end
|
54
|
+
end
|
55
|
+
|
56
|
+
def listening?(name)
|
57
|
+
listeners_for(name).any?
|
58
|
+
end
|
59
|
+
|
60
|
+
# This is a sync queue, so there is no waiting.
|
61
|
+
def wait
|
62
|
+
end
|
63
|
+
|
64
|
+
module Subscribers # :nodoc:
|
65
|
+
def self.new(pattern, listener)
|
66
|
+
if listener.respond_to?(:call)
|
67
|
+
subscriber = Timed.new pattern, listener
|
68
|
+
elsif listener.respond_to?(:measure)
|
69
|
+
subscriber = Evented.new pattern, listener
|
70
|
+
else
|
71
|
+
subscriber = LegacyEvented.new pattern, listener
|
72
|
+
end
|
73
|
+
|
74
|
+
unless pattern
|
75
|
+
AllMessages.new(subscriber)
|
76
|
+
else
|
77
|
+
subscriber
|
78
|
+
end
|
79
|
+
end
|
80
|
+
|
81
|
+
class Evented #:nodoc:
|
82
|
+
def initialize(pattern, delegate)
|
83
|
+
@pattern = pattern
|
84
|
+
@delegate = delegate
|
85
|
+
end
|
86
|
+
|
87
|
+
def start(name, id, payload)
|
88
|
+
@delegate.start name, id, payload
|
89
|
+
end
|
90
|
+
|
91
|
+
def finish(name, id, payload)
|
92
|
+
@delegate.finish name, id, payload
|
93
|
+
end
|
94
|
+
|
95
|
+
def measure(name, id, payload)
|
96
|
+
@delegate.measure(name, id, payload)
|
97
|
+
end
|
98
|
+
|
99
|
+
def subscribed_to?(name)
|
100
|
+
@pattern === name.to_s
|
101
|
+
end
|
102
|
+
|
103
|
+
def matches?(subscriber_or_name)
|
104
|
+
self === subscriber_or_name ||
|
105
|
+
@pattern && @pattern === subscriber_or_name
|
106
|
+
end
|
107
|
+
end
|
108
|
+
|
109
|
+
class LegacyEvented < Evented
|
110
|
+
def measure(name, id, payload)
|
111
|
+
start(name, id, payload)
|
112
|
+
finish(name, id, payload)
|
113
|
+
end
|
114
|
+
end
|
115
|
+
|
116
|
+
class Timed < LegacyEvented
|
117
|
+
def initialize(pattern, delegate)
|
118
|
+
@timestack = []
|
119
|
+
super
|
120
|
+
end
|
121
|
+
|
122
|
+
def publish(name, *args)
|
123
|
+
@delegate.call name, *args
|
124
|
+
end
|
125
|
+
|
126
|
+
def start(name, id, payload)
|
127
|
+
@timestack.push Time.now
|
128
|
+
end
|
129
|
+
|
130
|
+
def finish(name, id, payload)
|
131
|
+
started = @timestack.pop
|
132
|
+
@delegate.call(name, started, Time.now, id, payload)
|
133
|
+
end
|
134
|
+
end
|
135
|
+
|
136
|
+
class AllMessages # :nodoc:
|
137
|
+
def initialize(delegate)
|
138
|
+
@delegate = delegate
|
139
|
+
end
|
140
|
+
|
141
|
+
def start(name, id, payload)
|
142
|
+
@delegate.start name, id, payload
|
143
|
+
end
|
144
|
+
|
145
|
+
def finish(name, id, payload)
|
146
|
+
@delegate.finish name, id, payload
|
147
|
+
end
|
148
|
+
|
149
|
+
def measure(name, id, payload)
|
150
|
+
@delegate.measure name, id, payload
|
151
|
+
end
|
152
|
+
|
153
|
+
def publish(name, *args)
|
154
|
+
@delegate.publish name, *args
|
155
|
+
end
|
156
|
+
|
157
|
+
def subscribed_to?(name)
|
158
|
+
true
|
159
|
+
end
|
160
|
+
|
161
|
+
alias :matches? :===
|
162
|
+
end
|
163
|
+
end
|
164
|
+
end
|
165
|
+
end
|
166
|
+
end
|
@@ -0,0 +1,65 @@
|
|
1
|
+
require 'securerandom'
|
2
|
+
|
3
|
+
module ActiveSupport
|
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
|
+
if block_given?
|
19
|
+
@notifier.start(name, @id, payload)
|
20
|
+
begin
|
21
|
+
yield payload
|
22
|
+
rescue Exception => e
|
23
|
+
payload[:exception] = [e.class.name, e.message]
|
24
|
+
raise e
|
25
|
+
ensure
|
26
|
+
@notifier.finish(name, @id, payload)
|
27
|
+
end
|
28
|
+
else
|
29
|
+
@notifier.measure(name, @id, payload)
|
30
|
+
end
|
31
|
+
end
|
32
|
+
|
33
|
+
private
|
34
|
+
def unique_id
|
35
|
+
SecureRandom.hex(10)
|
36
|
+
end
|
37
|
+
end
|
38
|
+
|
39
|
+
class Event
|
40
|
+
attr_reader :name, :time, :transaction_id, :payload, :children
|
41
|
+
attr_accessor :end
|
42
|
+
|
43
|
+
def initialize(name, start, ending, transaction_id, payload)
|
44
|
+
@name = name
|
45
|
+
@payload = payload.dup
|
46
|
+
@time = start
|
47
|
+
@transaction_id = transaction_id
|
48
|
+
@end = ending
|
49
|
+
@children = []
|
50
|
+
end
|
51
|
+
|
52
|
+
def duration
|
53
|
+
1000.0 * (self.end - time)
|
54
|
+
end
|
55
|
+
|
56
|
+
def <<(event)
|
57
|
+
@children << event
|
58
|
+
end
|
59
|
+
|
60
|
+
def parent_of?(event)
|
61
|
+
@children.include? event
|
62
|
+
end
|
63
|
+
end
|
64
|
+
end
|
65
|
+
end
|