skylight 0.0.2
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/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
@@ -0,0 +1,73 @@
|
|
1
|
+
module Skylight
|
2
|
+
module Util
|
3
|
+
class AtomicRef
|
4
|
+
def initialize(v = nil)
|
5
|
+
@v = v
|
6
|
+
@m = Mutex.new
|
7
|
+
end
|
8
|
+
|
9
|
+
def get
|
10
|
+
@m.synchronize { @v }
|
11
|
+
end
|
12
|
+
|
13
|
+
def set(v)
|
14
|
+
@m.synchronize { @v = v }
|
15
|
+
end
|
16
|
+
|
17
|
+
def compare_and_set(expect, v)
|
18
|
+
@m.synchronize do
|
19
|
+
return false unless @v == expect
|
20
|
+
@v = v
|
21
|
+
end
|
22
|
+
|
23
|
+
true
|
24
|
+
end
|
25
|
+
|
26
|
+
def get_and_set(v)
|
27
|
+
while true
|
28
|
+
c = get
|
29
|
+
return c if compare_and_set(c, v)
|
30
|
+
end
|
31
|
+
end
|
32
|
+
end
|
33
|
+
|
34
|
+
class AtomicInteger < AtomicRef
|
35
|
+
|
36
|
+
def initialize(v = 0)
|
37
|
+
super(v)
|
38
|
+
end
|
39
|
+
|
40
|
+
def add_and_get(delta)
|
41
|
+
while true
|
42
|
+
c = get
|
43
|
+
n = c + delta
|
44
|
+
return n if compare_and_set(c, n)
|
45
|
+
end
|
46
|
+
end
|
47
|
+
|
48
|
+
def increment_and_get
|
49
|
+
add_and_get(1)
|
50
|
+
end
|
51
|
+
|
52
|
+
def decrement_and_get
|
53
|
+
add_and_get(-1)
|
54
|
+
end
|
55
|
+
|
56
|
+
def get_and_add(delta)
|
57
|
+
while true
|
58
|
+
c = get
|
59
|
+
n = c + delta
|
60
|
+
return c if compare_and_set(c, n)
|
61
|
+
end
|
62
|
+
end
|
63
|
+
|
64
|
+
def get_and_increment
|
65
|
+
get_and_add(1)
|
66
|
+
end
|
67
|
+
|
68
|
+
def get_and_decrement
|
69
|
+
get_and_add(-1)
|
70
|
+
end
|
71
|
+
end
|
72
|
+
end
|
73
|
+
end
|
@@ -0,0 +1,40 @@
|
|
1
|
+
module Skylight
|
2
|
+
module Util
|
3
|
+
class OutOfRangeError < RuntimeError; end
|
4
|
+
|
5
|
+
module Bytes
|
6
|
+
# Helper consts
|
7
|
+
MinUint64 = 0
|
8
|
+
MaxUint64 = (1<<64)-1
|
9
|
+
MinInt64 = -(1<<63)
|
10
|
+
MaxInt64 = (1<<63)-1
|
11
|
+
|
12
|
+
# varints
|
13
|
+
def append_uint64(buf, n)
|
14
|
+
if n < MinUint64 || n > MaxUint64
|
15
|
+
raise OutOfRangeError, n
|
16
|
+
end
|
17
|
+
|
18
|
+
while true
|
19
|
+
bits = n & 0x7F
|
20
|
+
n >>= 7
|
21
|
+
|
22
|
+
if n == 0
|
23
|
+
return buf << bits
|
24
|
+
end
|
25
|
+
|
26
|
+
buf << (bits | 0x80)
|
27
|
+
end
|
28
|
+
end
|
29
|
+
|
30
|
+
def str_bytesize(str)
|
31
|
+
str.bytesize
|
32
|
+
end
|
33
|
+
|
34
|
+
def append_string(buf, str)
|
35
|
+
append_uint64(buf, str_bytesize(str))
|
36
|
+
buf << str
|
37
|
+
end
|
38
|
+
end
|
39
|
+
end
|
40
|
+
end
|
@@ -0,0 +1,37 @@
|
|
1
|
+
module Skylight
|
2
|
+
module Util
|
3
|
+
class Clock
|
4
|
+
MICROSEC_PER_SEC = 1.to_f / 1_000_000
|
5
|
+
|
6
|
+
# Resolution is in seconds
|
7
|
+
def initialize(resolution)
|
8
|
+
@resolution = resolution
|
9
|
+
@usec_mult = MICROSEC_PER_SEC / resolution
|
10
|
+
end
|
11
|
+
|
12
|
+
def now
|
13
|
+
at(Time.now)
|
14
|
+
end
|
15
|
+
|
16
|
+
def at(time)
|
17
|
+
sec = time.to_i / @resolution
|
18
|
+
usec = time.usec * @usec_mult
|
19
|
+
(sec + usec).floor
|
20
|
+
end
|
21
|
+
|
22
|
+
def convert(secs)
|
23
|
+
(secs / @resolution).floor
|
24
|
+
end
|
25
|
+
|
26
|
+
def to_seconds(clock_val)
|
27
|
+
(clock_val * @resolution).floor
|
28
|
+
end
|
29
|
+
end
|
30
|
+
|
31
|
+
@clock = Clock.new(0.0001)
|
32
|
+
|
33
|
+
def self.clock
|
34
|
+
@clock
|
35
|
+
end
|
36
|
+
end
|
37
|
+
end
|
@@ -0,0 +1,32 @@
|
|
1
|
+
module Skylight
|
2
|
+
module Util
|
3
|
+
class EWMA
|
4
|
+
|
5
|
+
attr_reader :rate
|
6
|
+
|
7
|
+
def initialize(alpha)
|
8
|
+
@alpha = alpha
|
9
|
+
@uncounted = AtomicInteger.new
|
10
|
+
@rate = nil
|
11
|
+
end
|
12
|
+
|
13
|
+
def update(n)
|
14
|
+
@uncounted.add_and_get(n)
|
15
|
+
end
|
16
|
+
|
17
|
+
# Mark the passage of time and decay the current rate accordingly.
|
18
|
+
# This method is obviously not thread-safe as is expected to be
|
19
|
+
# invoked once every interval
|
20
|
+
def tick()
|
21
|
+
count = @uncounted.get_and_set(0)
|
22
|
+
instantRate = count
|
23
|
+
|
24
|
+
if rate
|
25
|
+
rate += (alpha * (instantRate - rate))
|
26
|
+
else
|
27
|
+
rate = instantRate
|
28
|
+
end
|
29
|
+
end
|
30
|
+
end
|
31
|
+
end
|
32
|
+
end
|
@@ -0,0 +1,93 @@
|
|
1
|
+
require 'thread'
|
2
|
+
|
3
|
+
module Skylight
|
4
|
+
module Util
|
5
|
+
# Simple thread-safe queue backed by a ring buffer. Will only block when
|
6
|
+
# poping.
|
7
|
+
class Queue
|
8
|
+
|
9
|
+
def initialize(max)
|
10
|
+
unless max > 0
|
11
|
+
raise ArgumentError, "queue size must be positive"
|
12
|
+
end
|
13
|
+
|
14
|
+
@max = max
|
15
|
+
@values = [nil] * max
|
16
|
+
@consume = 0
|
17
|
+
@produce = 0
|
18
|
+
@waiting = []
|
19
|
+
@mutex = Mutex.new
|
20
|
+
end
|
21
|
+
|
22
|
+
def empty?
|
23
|
+
@mutex.synchronize { __empty? }
|
24
|
+
end
|
25
|
+
|
26
|
+
def length
|
27
|
+
@mutex.synchronize { __length }
|
28
|
+
end
|
29
|
+
|
30
|
+
# Returns true if the item was queued, false otherwise
|
31
|
+
def push(obj)
|
32
|
+
@mutex.synchronize do
|
33
|
+
return false if __length == @max
|
34
|
+
@values[@produce] = obj
|
35
|
+
@produce = (@produce + 1) % @max
|
36
|
+
|
37
|
+
# Wakeup a blocked thread
|
38
|
+
begin
|
39
|
+
t = @waiting.shift
|
40
|
+
t.wakeup if t
|
41
|
+
rescue ThreadError
|
42
|
+
retry
|
43
|
+
end
|
44
|
+
end
|
45
|
+
|
46
|
+
true
|
47
|
+
end
|
48
|
+
|
49
|
+
def pop(timeout = nil)
|
50
|
+
if timeout && timeout < 0
|
51
|
+
raise ArgumentError, "timeout must be nil or >= than 0"
|
52
|
+
end
|
53
|
+
|
54
|
+
@mutex.synchronize do
|
55
|
+
if __empty?
|
56
|
+
if !timeout || timeout > 0
|
57
|
+
t = Thread.current
|
58
|
+
@waiting << t
|
59
|
+
@mutex.sleep(timeout)
|
60
|
+
# Ensure that the thread is not in the waiting list
|
61
|
+
@waiting.delete(t)
|
62
|
+
else
|
63
|
+
return
|
64
|
+
end
|
65
|
+
end
|
66
|
+
|
67
|
+
__pop unless __empty?
|
68
|
+
end
|
69
|
+
end
|
70
|
+
|
71
|
+
private
|
72
|
+
|
73
|
+
def __length
|
74
|
+
((@produce - @consume) % @max)
|
75
|
+
end
|
76
|
+
|
77
|
+
def __empty?
|
78
|
+
@produce == @consume
|
79
|
+
end
|
80
|
+
|
81
|
+
def __pop
|
82
|
+
i = @consume
|
83
|
+
v = @values[i]
|
84
|
+
|
85
|
+
@values[i] = nil
|
86
|
+
@consume = (i + 1) % @max
|
87
|
+
|
88
|
+
return v
|
89
|
+
end
|
90
|
+
|
91
|
+
end
|
92
|
+
end
|
93
|
+
end
|
@@ -0,0 +1,63 @@
|
|
1
|
+
module Skylight
|
2
|
+
module Util
|
3
|
+
class UniformSample
|
4
|
+
include Enumerable
|
5
|
+
|
6
|
+
attr_reader :size, :count
|
7
|
+
|
8
|
+
def initialize(size)
|
9
|
+
@size = size
|
10
|
+
@count = 0
|
11
|
+
@values = []
|
12
|
+
end
|
13
|
+
|
14
|
+
def clear
|
15
|
+
@count = 0
|
16
|
+
@values.clear
|
17
|
+
self
|
18
|
+
end
|
19
|
+
|
20
|
+
def length
|
21
|
+
@size < @count ? @size : @count
|
22
|
+
end
|
23
|
+
|
24
|
+
def empty?
|
25
|
+
@count == 0
|
26
|
+
end
|
27
|
+
|
28
|
+
def each
|
29
|
+
i = 0
|
30
|
+
to = length
|
31
|
+
|
32
|
+
while i < to
|
33
|
+
yield @values[i]
|
34
|
+
i += 1
|
35
|
+
end
|
36
|
+
|
37
|
+
self
|
38
|
+
end
|
39
|
+
|
40
|
+
def <<(v)
|
41
|
+
if idx = increment!
|
42
|
+
@values[idx] = v
|
43
|
+
end
|
44
|
+
|
45
|
+
self
|
46
|
+
end
|
47
|
+
|
48
|
+
private
|
49
|
+
|
50
|
+
def increment!
|
51
|
+
c = (@count += 1)
|
52
|
+
|
53
|
+
if (c <= @size)
|
54
|
+
c - 1
|
55
|
+
else
|
56
|
+
r = rand(@count)
|
57
|
+
r if r < @size
|
58
|
+
end
|
59
|
+
end
|
60
|
+
|
61
|
+
end
|
62
|
+
end
|
63
|
+
end
|
@@ -0,0 +1,33 @@
|
|
1
|
+
module Skylight
|
2
|
+
module Util
|
3
|
+
class UUID
|
4
|
+
BYTE_SIZE = 16
|
5
|
+
PREFIX_SIZE = 8
|
6
|
+
|
7
|
+
def self.gen(prefix = nil)
|
8
|
+
if prefix == nil
|
9
|
+
return SecureRandom.random_bytes(BYTE_SIZE)
|
10
|
+
end
|
11
|
+
|
12
|
+
if prefix.bytesize > PREFIX_SIZE
|
13
|
+
raise "UUID prefix must be less than 8 bytes"
|
14
|
+
end
|
15
|
+
|
16
|
+
# Does not fully conform with the spec
|
17
|
+
rnd = SecureRandom.random_bytes(BYTE_SIZE - prefix.bytesize)
|
18
|
+
new "#{prefix}#{rnd}"
|
19
|
+
end
|
20
|
+
|
21
|
+
attr_reader :bytes
|
22
|
+
|
23
|
+
def initialize(bytes)
|
24
|
+
@bytes = bytes
|
25
|
+
end
|
26
|
+
|
27
|
+
def to_s
|
28
|
+
@to_s ||= ""
|
29
|
+
end
|
30
|
+
|
31
|
+
end
|
32
|
+
end
|
33
|
+
end
|
@@ -0,0 +1,232 @@
|
|
1
|
+
module Skylight
|
2
|
+
class Worker
|
3
|
+
CONTENT_ENCODING = 'content-encoding'.freeze
|
4
|
+
CONTENT_LENGTH = 'content-length'.freeze
|
5
|
+
CONTENT_TYPE = 'content-type'.freeze
|
6
|
+
APPLICATION_JSON = 'application/json'.freeze
|
7
|
+
DIREWOLF_REPORT = 'application/x-direwolf-report'.freeze
|
8
|
+
AUTHORIZATION = 'authorization'.freeze
|
9
|
+
ENDPOINT = '/report'.freeze
|
10
|
+
DEFLATE = 'deflate'.freeze
|
11
|
+
GZIP = 'gzip'.freeze
|
12
|
+
FLUSH_DELAY = Util.clock.convert(0.5)
|
13
|
+
|
14
|
+
class Batch
|
15
|
+
attr_reader :from, :counts, :sample
|
16
|
+
|
17
|
+
def initialize(config, from, interval)
|
18
|
+
@config = config
|
19
|
+
@interval = interval
|
20
|
+
@from = from
|
21
|
+
@to = from + interval
|
22
|
+
@flush_at = @to + FLUSH_DELAY
|
23
|
+
@sample = Util::UniformSample.new(config.samples_per_interval)
|
24
|
+
@counts = Hash.new { |h,k| h[k] = 0 }
|
25
|
+
end
|
26
|
+
|
27
|
+
def should_flush?(now)
|
28
|
+
now >= @flush_at
|
29
|
+
end
|
30
|
+
|
31
|
+
def next_batch
|
32
|
+
Batch.new(@config, @to, @interval)
|
33
|
+
end
|
34
|
+
|
35
|
+
def empty?
|
36
|
+
@sample.empty?
|
37
|
+
end
|
38
|
+
|
39
|
+
def wants?(trace)
|
40
|
+
return trace.to >= @from && trace.to < @to
|
41
|
+
end
|
42
|
+
|
43
|
+
def push(trace)
|
44
|
+
# Count it
|
45
|
+
@counts[trace.endpoint] += 1
|
46
|
+
# Push the trace into the sample
|
47
|
+
@sample << trace
|
48
|
+
end
|
49
|
+
end
|
50
|
+
|
51
|
+
attr_reader :instrumenter, :connection
|
52
|
+
|
53
|
+
def initialize(instrumenter)
|
54
|
+
@instrumenter = instrumenter
|
55
|
+
@interval = config.interval
|
56
|
+
@protocol = config.protocol
|
57
|
+
|
58
|
+
reset
|
59
|
+
end
|
60
|
+
|
61
|
+
def start!
|
62
|
+
shutdown! if @thread
|
63
|
+
@thread = Thread.new { work }
|
64
|
+
self
|
65
|
+
end
|
66
|
+
|
67
|
+
def shutdown!
|
68
|
+
# Don't do anything if the worker isn't running
|
69
|
+
return self unless @thread
|
70
|
+
|
71
|
+
thread = @thread
|
72
|
+
@thread = nil
|
73
|
+
|
74
|
+
@queue.push(:SHUTDOWN)
|
75
|
+
|
76
|
+
unless thread.join(5)
|
77
|
+
begin
|
78
|
+
# FORCE KILL!!
|
79
|
+
thread.kill
|
80
|
+
rescue ThreadError
|
81
|
+
end
|
82
|
+
end
|
83
|
+
|
84
|
+
reset
|
85
|
+
self
|
86
|
+
end
|
87
|
+
|
88
|
+
def submit(trace)
|
89
|
+
return unless @thread
|
90
|
+
@queue.push(trace)
|
91
|
+
end
|
92
|
+
|
93
|
+
# A worker iteration
|
94
|
+
def iter(msg, now=Util.clock.now)
|
95
|
+
unless @current_batch
|
96
|
+
interval = Util.clock.convert(@interval)
|
97
|
+
from = (now / interval) * interval
|
98
|
+
|
99
|
+
# If we're still accepting traces from the previous batch
|
100
|
+
# Create the previous interval instead
|
101
|
+
if now < from + FLUSH_DELAY
|
102
|
+
from -= interval
|
103
|
+
end
|
104
|
+
|
105
|
+
@current_batch = Batch.new(config, from, interval)
|
106
|
+
@next_batch = @current_batch.next_batch
|
107
|
+
end
|
108
|
+
|
109
|
+
if msg == :SHUTDOWN
|
110
|
+
flush(@current_batch) if @current_batch
|
111
|
+
flush(@next_batch) if @next_batch
|
112
|
+
return false
|
113
|
+
end
|
114
|
+
|
115
|
+
while @current_batch && @current_batch.should_flush?(now)
|
116
|
+
flush(@current_batch)
|
117
|
+
@current_batch = @next_batch
|
118
|
+
@next_batch = @current_batch.next_batch
|
119
|
+
end
|
120
|
+
|
121
|
+
if Trace === msg
|
122
|
+
debug "Received trace"
|
123
|
+
if @current_batch.wants?(msg)
|
124
|
+
@current_batch.push(msg)
|
125
|
+
elsif @next_batch.wants?(msg)
|
126
|
+
@next_batch.push(msg)
|
127
|
+
else
|
128
|
+
# Seems bad bro
|
129
|
+
end
|
130
|
+
end
|
131
|
+
|
132
|
+
true
|
133
|
+
end
|
134
|
+
|
135
|
+
private
|
136
|
+
|
137
|
+
def config
|
138
|
+
@instrumenter.config
|
139
|
+
end
|
140
|
+
|
141
|
+
def logger
|
142
|
+
config.logger
|
143
|
+
end
|
144
|
+
|
145
|
+
def reset
|
146
|
+
@queue = Util::Queue.new(config.max_pending_traces)
|
147
|
+
end
|
148
|
+
|
149
|
+
def work
|
150
|
+
loop do
|
151
|
+
msg = @queue.pop(@interval.to_f / 20)
|
152
|
+
success = iter(msg)
|
153
|
+
return if !success
|
154
|
+
end
|
155
|
+
rescue Exception => e
|
156
|
+
logger.error "[SKYLIGHT] #{e.message} - #{e.class} - #{e.backtrace.first}"
|
157
|
+
if logger.debug?
|
158
|
+
logger.debug(e.backtrace.join("\n"))
|
159
|
+
end
|
160
|
+
end
|
161
|
+
|
162
|
+
attr_reader :sample_starts_at, :interval
|
163
|
+
|
164
|
+
def flush(batch)
|
165
|
+
return if batch.empty?
|
166
|
+
# Skip if there is no authentication token
|
167
|
+
return unless config.authentication_token
|
168
|
+
|
169
|
+
debug "Flushing: #{batch}"
|
170
|
+
|
171
|
+
body = ''
|
172
|
+
# write the body
|
173
|
+
@protocol.write(body, batch.from, batch.counts, batch.sample)
|
174
|
+
|
175
|
+
if config.deflate?
|
176
|
+
body = Util::Gzip.compress(body)
|
177
|
+
end
|
178
|
+
|
179
|
+
# send
|
180
|
+
http_post(body)
|
181
|
+
end
|
182
|
+
|
183
|
+
def http_post(body)
|
184
|
+
req = http_request(body.bytesize)
|
185
|
+
req.body = body
|
186
|
+
|
187
|
+
debug "Posting report to server"
|
188
|
+
http = Net::HTTP.new config.host, config.port
|
189
|
+
http.use_ssl = true if config.ssl?
|
190
|
+
|
191
|
+
http.start do |http|
|
192
|
+
resp = http.request req
|
193
|
+
|
194
|
+
unless resp.code == '200'
|
195
|
+
debug "Server responded with #{resp.code}"
|
196
|
+
end
|
197
|
+
end
|
198
|
+
|
199
|
+
true
|
200
|
+
rescue => e
|
201
|
+
logger.error "[SKYLIGHT] #{e.message} - #{e.class} - #{e.backtrace.first}"
|
202
|
+
if logger.debug?
|
203
|
+
logger.debug(e.backtrace.join("\n"))
|
204
|
+
end
|
205
|
+
end
|
206
|
+
|
207
|
+
def http_request(length)
|
208
|
+
hdrs = {}
|
209
|
+
|
210
|
+
hdrs[CONTENT_LENGTH] = length.to_s
|
211
|
+
hdrs[AUTHORIZATION] = config.authentication_token
|
212
|
+
hdrs[CONTENT_TYPE] = APPLICATION_JSON
|
213
|
+
|
214
|
+
if config.deflate?
|
215
|
+
hdrs[CONTENT_ENCODING] = GZIP
|
216
|
+
end
|
217
|
+
|
218
|
+
Net::HTTPGenericRequest.new \
|
219
|
+
'POST', # Request method
|
220
|
+
true, # There is a request body
|
221
|
+
true, # There is a response body
|
222
|
+
ENDPOINT, # Endpoint
|
223
|
+
hdrs
|
224
|
+
end
|
225
|
+
|
226
|
+
def debug(msg)
|
227
|
+
return unless logger && logger.debug?
|
228
|
+
logger.debug "[SKYLIGHT] #{msg}"
|
229
|
+
end
|
230
|
+
|
231
|
+
end
|
232
|
+
end
|