skylight 0.0.2

Sign up to get free protection for your applications and to get access to all the features.
@@ -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