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.
@@ -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,15 @@
1
+ require 'zlib'
2
+
3
+ module Skylight
4
+ module Util
5
+ module Gzip
6
+ def self.compress(str)
7
+ output = StringIO.new
8
+ gz = Zlib::GzipWriter.new(output)
9
+ gz.write(str)
10
+ gz.close
11
+ output.string
12
+ end
13
+ end
14
+ end
15
+ 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,3 @@
1
+ module Skylight
2
+ VERSION = '0.0.2'
3
+ 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