skylight 0.2.2 → 0.2.3

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.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: 54d9a76b353b8d0a2a6fecce094a8a32802a1b9d
4
- data.tar.gz: c773bed69979daa2fe511a1fef1698699aa6ba42
3
+ metadata.gz: 38880fd1b62fad365c461e3fb6517e979f14baa6
4
+ data.tar.gz: 9c62ce619478170a9e1b38e788bc35e61cd41b94
5
5
  SHA512:
6
- metadata.gz: ad2f6c9f9edec7ec1417a89faa026cafbb5b36b9adc6e47c35063793178b8ff4bd75d683c404463fb37e0581be526eeeb7443e9df2314be060fd78d9419a51b6
7
- data.tar.gz: f4b98d9d9b50b111b7c5f33ae57343c7985be52bcaa0cced2781c322bc1231b2186dd8f1ffda1a3b9eb098afec2c98e3572ed3492e21c39b4c8693acd3399dc8
6
+ metadata.gz: f0aaab6c04ff20b4fcb06da895808f9c84ae9fda59fd33a62639355b9ae5933ad417a80224c2409de46121a5dcd6fe8d45d09434fbd02d04cf7007659e6a0254
7
+ data.tar.gz: 2c7a866a02d2746130638408246c9d751a46dd7395f6ae98533081e984a7dffd3334de731a8dfa5b88a52b019af25e69c0a32f463d202f0dfa6b68ac7ffb68d8
@@ -1,3 +1,9 @@
1
+ ## 0.2.3 (December 20, 2013)
2
+
3
+ * Fix SQL lexing for comments, arrays, double-colon casting, and multiple queries
4
+ * Handle template paths from gems
5
+ * Status and exception reports for agent debugging
6
+
1
7
  ## 0.2.2 (December 10, 2013)
2
8
 
3
9
  * Added support for Mongoid/Moped
@@ -30,6 +30,7 @@ module Skylight
30
30
  'AGENT_SAMPLE_SIZE' => :'agent.sample',
31
31
  'AGENT_SOCKFILE_PATH' => :'agent.sockfile_path',
32
32
  'AGENT_STRATEGY' => :'agent.strategy',
33
+ 'AGENT_MAX_MEMORY' => :'agent.max_memory',
33
34
  'REPORT_HOST' => :'report.host',
34
35
  'REPORT_PORT' => :'report.port',
35
36
  'REPORT_SSL' => :'report.ssl',
@@ -49,6 +50,7 @@ module Skylight
49
50
  :'agent.keepalive' => 60,
50
51
  :'agent.interval' => 5,
51
52
  :'agent.sample' => 200,
53
+ :'agent.max_memory' => 256, # MB
52
54
  :'report.host' => 'agent.skylight.io'.freeze,
53
55
  :'report.port' => 443,
54
56
  :'report.ssl' => true,
@@ -79,7 +79,7 @@ module Skylight
79
79
  self
80
80
 
81
81
  rescue Exception => e
82
- error "failed to start instrumenter; msg=%s", e.message
82
+ log_error "failed to start instrumenter; msg=%s", e.message
83
83
  nil
84
84
  end
85
85
 
@@ -99,7 +99,7 @@ module Skylight
99
99
  begin
100
100
  trace = Messages::Trace::Builder.new(self, endpoint, Util::Clock.micros, cat, title, desc, annot)
101
101
  rescue Exception => e
102
- error e.message
102
+ log_error e.message
103
103
  t { e.backtrace.join("\n") }
104
104
  return
105
105
  end
@@ -183,14 +183,10 @@ module Skylight
183
183
  end
184
184
  end
185
185
 
186
- def error(reason, body)
187
- t { fmt "processing error; reason=%s; body=%s", reason, body }
186
+ def error(type, description, details=nil)
187
+ t { fmt "processing error; type=%s; description=%s", type, description }
188
188
 
189
- if body.encoding == Encoding::BINARY || !body.valid_encoding?
190
- body = Base64.encode64(body)
191
- end
192
-
193
- message = Skylight::Messages::Error.new(reason: reason, body: body)
189
+ message = Skylight::Messages::Error.new(type: type, description: description, details: details && details.to_json)
194
190
 
195
191
  unless @worker.submit(message)
196
192
  warn "failed to submit error to worker"
@@ -3,8 +3,9 @@ require 'skylight/messages/base'
3
3
  module Skylight
4
4
  module Messages
5
5
  class Error < Base
6
- required :reason, :string, 1
7
- required :body, :string, 2
6
+ required :type, :string, 1
7
+ required :description, :string, 2
8
+ optional :details, :string, 3
8
9
  end
9
10
  end
10
11
  end
@@ -60,11 +60,21 @@ module Skylight
60
60
  return path if relative_path?(path)
61
61
 
62
62
  root = array_find(@paths) { |p| path.start_with?(p) }
63
+ type = :project
64
+
65
+ unless root
66
+ root = array_find(Gem.path) { |p| path.start_with?(p) }
67
+ type = :gem
68
+ end
63
69
 
64
70
  if root
65
71
  start = root.size
66
72
  start += 1 if path.getbyte(start) == SEPARATOR_BYTE
67
- path[start, path.size]
73
+ if type == :gem
74
+ "$GEM_PATH/#{path[start, path.size]}"
75
+ else
76
+ path[start, path.size]
77
+ end
68
78
  else
69
79
  annotations[:skylight_error] = ["absolute_path", path]
70
80
  "Absolute Path"
@@ -45,9 +45,8 @@ module Skylight
45
45
  title, sql, binds = SqlLexer::Lexer.bindify(payload[:sql], precalculated)
46
46
  [ title, sql, binds, nil ]
47
47
  rescue
48
- # Encode this since we may have improperly incoded strings that can't be to_json'ed
49
- encoded = encode(payload: payload, precalculated: precalculated)
50
- [ nil, nil, nil, ["sql_parse", encoded.to_json] ]
48
+ error = ["sql_parse", encode(payload[:sql]), encode(payload: payload, precalculated: precalculated)]
49
+ [ nil, nil, nil, error ]
51
50
  end
52
51
 
53
52
  def encode(body)
@@ -87,8 +87,8 @@ module Skylight
87
87
  end
88
88
 
89
89
  def execute(req, body=nil)
90
- t { fmt "executing HTTP request; host=%s; port=%s; body=%s",
91
- @host, @port, body && body.bytesize }
90
+ t { fmt "executing HTTP request; host=%s; port=%s; path=%s, body=%s",
91
+ @host, @port, req.path, body && body.bytesize }
92
92
 
93
93
  if body
94
94
  body = Gzip.compress(body) if @deflate
@@ -35,7 +35,13 @@ module Skylight
35
35
  log :error, msg, *args
36
36
  end
37
37
 
38
- alias fmt sprintf
38
+ alias log_trace trace
39
+ alias log_debug debug
40
+ alias log_info info
41
+ alias log_warn warn
42
+ alias log_error error
43
+
44
+ alias fmt sprintf
39
45
 
40
46
  def log(level, msg, *args)
41
47
  return unless respond_to?(:config)
@@ -1,4 +1,4 @@
1
1
  module Skylight
2
- VERSION = '0.2.2'
2
+ VERSION = '0.2.3'
3
3
  end
4
4
 
@@ -62,27 +62,37 @@ module Skylight
62
62
  true
63
63
  end
64
64
 
65
+ def send_status(status)
66
+ post_data(:status, status)
67
+ end
68
+
69
+ def send_exception(exception)
70
+ post_data(:exception, exception)
71
+ end
72
+
65
73
  private
66
74
 
67
- def send_error(msg)
68
- res = @http_auth.post("/agent/error?hostname=#{escape(config[:'hostname'])}", reason: msg.reason, body: msg.body)
75
+ def post_data(type, data)
76
+ t { "posting data (#{type}): #{data.inspect}" }
77
+
78
+ res = @http_auth.post("/agent/#{type}?hostname=#{escape(config[:'hostname'])}", data)
69
79
 
70
80
  # error already handled in Util::HTTP
71
81
  return unless res
72
82
 
73
83
  unless res.success?
74
- if (400..499).include? res.status
75
- warn "error wasn't sent successfully; status=%s", res.status
76
- end
77
-
78
- warn "could not fetch report session token; status=%s", res.status
79
- return
84
+ warn "#{type} wasn't sent successfully; status=%s", res.status
80
85
  end
81
86
  rescue Exception => e
82
87
  error "exception; msg=%s; class=%s", e.message, e.class
83
88
  t { e.backtrace.join("\n") }
84
89
  end
85
90
 
91
+ def send_error(msg)
92
+ details = msg.details ? JSON.parse(msg.details) : nil
93
+ post_data(:error, type: msg.type, description: msg.description, details: details)
94
+ end
95
+
86
96
  def finish
87
97
  t { fmt "collector finishing up" }
88
98
 
@@ -2,8 +2,6 @@ require 'socket'
2
2
 
3
3
  module Skylight
4
4
  module Worker
5
- # TODO:
6
- # - Shutdown if no connections for over a minute
7
5
  class Server
8
6
  LOCKFILE_PATH = 'SKYLIGHT_LOCKFILE_PATH'.freeze
9
7
  LOCKFILE_ENV_KEY = 'SKYLIGHT_LOCKFILE_FD'.freeze
@@ -18,7 +16,10 @@ module Skylight
18
16
  :config,
19
17
  :keepalive,
20
18
  :lockfile_path,
21
- :sockfile_path
19
+ :sockfile_path,
20
+ :status_interval,
21
+ :last_status_update,
22
+ :max_memory
22
23
 
23
24
  def initialize(config, lockfile, srv, lockfile_path)
24
25
  unless lockfile && srv
@@ -37,6 +38,8 @@ module Skylight
37
38
  @connections = {}
38
39
  @lockfile_path = lockfile_path
39
40
  @sockfile_path = @config[:'agent.sockfile_path']
41
+ @status_interval = 60
42
+ @max_memory = @config[:'agent.max_memory']
40
43
  end
41
44
 
42
45
  # Called from skylight.rb on require
@@ -132,6 +135,7 @@ module Skylight
132
135
  now = Time.now.to_i
133
136
  next_sanity_check_at = now + tick
134
137
  had_client_at = now
138
+ last_status_update = now
135
139
 
136
140
  trace "starting IO loop"
137
141
  begin
@@ -179,9 +183,16 @@ module Skylight
179
183
  if keepalive < now - had_client_at
180
184
  info "no clients for #{keepalive} sec - shutting down"
181
185
  @run = false
182
- elsif next_sanity_check_at <= now
183
- next_sanity_check_at = now + tick
184
- sanity_check
186
+ else
187
+ if next_sanity_check_at <= now
188
+ next_sanity_check_at = now + tick
189
+ sanity_check
190
+ end
191
+
192
+ if status_interval < now - last_status_update
193
+ last_status_update = now
194
+ status_check
195
+ end
185
196
  end
186
197
 
187
198
  rescue SignalException => e
@@ -193,9 +204,11 @@ module Skylight
193
204
  rescue Exception => e
194
205
  error "Loop exception: %s (%s)", e.message, e.class
195
206
  puts e.backtrace
207
+ @collector.send_exception(class_name: e.class.name, message: e.message, backtrace: e.backtrace)
196
208
  return false
197
209
  rescue Object => o
198
210
  error "Unknown object thrown: `%s`", o.to_s
211
+ @collector.send_exception(class_name: o.class.name)
199
212
  return false
200
213
  end while @run
201
214
 
@@ -301,6 +314,22 @@ module Skylight
301
314
  raise WorkerStateError, "sockfile gone"
302
315
  end
303
316
  end
317
+
318
+ def status_check
319
+ memory_usage = get_memory_usage
320
+
321
+ @collector.send_status(memory: memory_usage, max_memory: max_memory)
322
+
323
+ if memory_usage > max_memory
324
+ raise WorkerStateError, "Memory limit exceeded: #{memory_usage} (max: #{max_memory})"
325
+ end
326
+ end
327
+
328
+ def get_memory_usage
329
+ `ps -o rss= -p #{Process.pid}`.to_i / 1024
330
+ rescue Errno::ENOENT
331
+ 0
332
+ end
304
333
  end
305
334
  end
306
335
  end
@@ -19,7 +19,10 @@ module SqlLexer
19
19
  End = %Q<;|$>
20
20
 
21
21
  InOp = %q<IN>
22
- SpecialOps = %Q<#{InOp}(?=[#{WS}])>
22
+ ArrayOp = %q<ARRAY>
23
+ ColonColonOp = %Q<::(?=[#{StartID}])>
24
+ ArrayIndexOp = %q<\\[(?:\-?\d+(?::\-?\d+)?|NULL)\\]>
25
+ SpecialOps = %Q<#{InOp}(?=[#{WS}])|#{ColonColonOp}|#{ArrayOp}|#{ArrayIndexOp}>
23
26
 
24
27
  StartQuotedID = %Q<">
25
28
  StartTickedID = %Q<`>
@@ -35,13 +38,14 @@ module SqlLexer
35
38
  StartAnyId = %Q<"#{StartID}>
36
39
  Placeholder = %q<\$\p{Digit}+>
37
40
 
38
- AfterID = %Q<[#{WS};#{StartNonBind}]|(?:#{OpPart})|$>
41
+ AfterID = %Q<[#{WS};#{StartNonBind}]|(?:#{OpPart})|(?:#{ColonColonOp})|(?:#{ArrayIndexOp})|$>
39
42
  ID = %Q<[#{StartID}][#{PartID}]*(?=#{AfterID})>
40
43
  AfterOp = %Q<[#{WS}]|[#{StartAnyId}]|[#{StartBind}]|(#{StartNonBind})|$>
41
44
  Op = %Q<(?:#{OpPart})+(?=#{AfterOp})>
42
45
  QuotedID = %Q<#{StartQuotedID}(?:[^"]|"")*">
43
46
  TickedID = %Q<#{StartTickedID}(?:[^`]|``)*`>
44
47
  NonBind = %Q<#{ID}|#{Op}|#{QuotedID}|#{TickedID}|#{Placeholder}>
48
+ Type = %Q<[#{StartID}][#{PartID}]*(?:\\(\d+\\)|\\[\\])?(?=#{AfterID})>
45
49
  QuotedTable = %Q<#{TickedID}|#{QuotedID}>
46
50
 
47
51
  StringBody = %q<(?:''|\x5C'|[^'])*>
@@ -62,8 +66,12 @@ module SqlLexer
62
66
  TkWS = %r<[#{WS}]+>u
63
67
  TkOptWS = %r<[#{WS}]*>u
64
68
  TkOp = %r<[#{OpPart}]>u
69
+ TkComment = %r<^#{OptWS}--.*$>u
70
+ TkBlockCommentStart = %r</\*>u
71
+ TkBlockCommentEnd = %r<\*/>u
65
72
  TkPlaceholder = %r<#{Placeholder}>u
66
73
  TkNonBind = %r<#{NonBind}>u
74
+ TkType = %r<#{Type}>u
67
75
  TkQuotedTable = %r<#{QuotedTable}>iu
68
76
  TkUpdateTable = %r<UPDATE#{TableNext}>iu
69
77
  TkInsertTable = %r<INSERT[#{WS}]+INTO#{TableNext}>iu
@@ -73,6 +81,9 @@ module SqlLexer
73
81
  TkEnd = %r<;?[#{WS}]*>u
74
82
  TkBind = %r<#{String}|#{Number}|#{Literals}>u
75
83
  TkIn = %r<#{InOp}>iu
84
+ TkColonColon = %r<#{ColonColonOp}>u
85
+ TkArray = %r<#{ArrayOp}>iu
86
+ TkArrayIndex = %r<#{ArrayIndexOp}>iu
76
87
  TkSpecialOp = %r<#{SpecialOps}>iu
77
88
  TkStartSelect = %r<SELECT(?=(?:[#{WS}]|#{OpPart}))>iu
78
89
 
@@ -86,7 +97,8 @@ module SqlLexer
86
97
  table_name: :process_table_name,
87
98
  end: :process_end,
88
99
  special: :process_special,
89
- in: :process_in
100
+ in: :process_in,
101
+ array: :process_array
90
102
  }
91
103
 
92
104
  def self.bindify(string, binds=nil)
@@ -158,6 +170,8 @@ module SqlLexer
158
170
  UNKNOWN = "<unknown>".freeze
159
171
 
160
172
  def process(binds)
173
+ process_comments
174
+
161
175
  @operation = nil
162
176
  @provided_binds = binds
163
177
 
@@ -201,6 +215,64 @@ module SqlLexer
201
215
 
202
216
  EMPTY = "".freeze
203
217
 
218
+ def process_comments
219
+ # SQL treats comments as similar to whitespace
220
+ # Here we replace all comments with spaces of the same length so as to not affect binds
221
+
222
+ # Remove block comments
223
+ # SQL allows for nested comments so this takes a bit more work
224
+ while @scanner.skip_until(TkBlockCommentStart)
225
+ count = 1
226
+ pos = @scanner.pos - 2
227
+
228
+ while true
229
+ # Determine whether we close the comment or start nesting
230
+ next_open = @scanner.skip_until(TkBlockCommentStart)
231
+ @scanner.unscan if next_open
232
+ next_close = @scanner.skip_until(TkBlockCommentEnd)
233
+ @scanner.unscan if next_close
234
+
235
+ if next_open && next_open < next_close
236
+ # We're nesting
237
+ count += 1
238
+ @scanner.skip_until(TkBlockCommentStart)
239
+ else
240
+ # We're closing
241
+ count -= 1
242
+ @scanner.skip_until(TkBlockCommentEnd)
243
+ end
244
+
245
+ if count > 10_000
246
+ raise "The SQL '#{@scanner.string}' could not be parsed because of too many iterations in block comments"
247
+ end
248
+
249
+ if count == 0
250
+ # We've closed all comments
251
+ length = @scanner.pos - pos
252
+ # Dup the string if necessary so we aren't destructive to the original value
253
+ @scanner.string = @input.dup if @scanner.string == @input
254
+ # Replace the comment with spaces
255
+ @scanner.string[pos, length] = SPACE*length
256
+ break
257
+ end
258
+ end
259
+ end
260
+
261
+ @scanner.reset
262
+
263
+ # Remove single line comments
264
+ while @scanner.skip_until(TkComment)
265
+ pos = @scanner.pos
266
+ len = @scanner.matched_size
267
+ # Dup the string if necessary so we aren't destructive to the original value
268
+ @scanner.string = @input.dup if @scanner.string == @input
269
+ # Replace the comment with spaces
270
+ @scanner.string[pos-len, len] = SPACE*len
271
+ end
272
+
273
+ @scanner.reset
274
+ end
275
+
204
276
  def process_begin
205
277
  @scanner.skip(TkOptWS)
206
278
  @state = :first_token
@@ -281,6 +353,17 @@ module SqlLexer
281
353
  @scanner.skip(TkOptWS)
282
354
  @scanner.skip(/\(/u)
283
355
  @state = :in
356
+ elsif @scanner.skip(TkArray)
357
+ @scanner.skip(/\[/u)
358
+ @state = :array
359
+ elsif @scanner.skip(TkColonColon)
360
+ if @scanner.skip(TkType)
361
+ @state = :tokens
362
+ else
363
+ @state = :end
364
+ end
365
+ elsif @scanner.skip(TkArrayIndex)
366
+ @state = :tokens
284
367
  end
285
368
  end
286
369
 
@@ -315,7 +398,57 @@ module SqlLexer
315
398
  process_tokens
316
399
  end
317
400
 
318
- send STATE_HANDLERS[@state]
401
+ __send__ STATE_HANDLERS[@state]
402
+ end
403
+
404
+ @binds << pos
405
+ @binds << @scanner.pos - pos
406
+
407
+ @skip_binds = false
408
+
409
+ @state = :tokens
410
+ end
411
+
412
+ def process_array
413
+ nest = 1
414
+ iterations = 0
415
+
416
+ @skip_binds = true
417
+ pos = @scanner.pos - 6
418
+
419
+ while nest > 0
420
+ iterations += 1
421
+
422
+ if iterations > 10_000
423
+ raise "The SQL '#{@scanner.string}' could not be parsed because of too many iterations in ARRAY"
424
+ end
425
+
426
+ if @debug
427
+ p "array loop"
428
+ p @state
429
+ p @scanner
430
+ end
431
+
432
+ if @scanner.skip(/\[/u)
433
+ nest += 1
434
+ elsif @scanner.skip(/\]/u)
435
+ nest -= 1
436
+
437
+ break if nest.zero?
438
+
439
+ # End of final nested array
440
+ next if @scanner.skip(/#{TkOptWS}(?=\])/u)
441
+ end
442
+
443
+ # A NULL array
444
+ next if @scanner.skip(/NULL/iu)
445
+
446
+ # Another nested array
447
+ next if @scanner.skip(/#{TkOptWS},#{TkOptWS}(?=\[)/u)
448
+
449
+ process_tokens
450
+
451
+ __send__ STATE_HANDLERS[@state]
319
452
  end
320
453
 
321
454
  @binds << pos
@@ -344,13 +477,18 @@ module SqlLexer
344
477
  end
345
478
 
346
479
  def process_end
347
- @scanner.skip(TkEnd)
480
+ if @scanner.skip(TkEnd)
481
+ if @scanner.eos?
482
+ @state = nil
483
+ else
484
+ process_tokens
485
+ end
486
+ end
348
487
 
349
- unless @scanner.eos?
488
+ # We didn't hit EOS and we couldn't process any tokens
489
+ if @state == :end
350
490
  raise "The SQL '#{@scanner.string}' could not be parsed"
351
491
  end
352
-
353
- @state = nil
354
492
  end
355
493
 
356
494
  private
@@ -1,3 +1,3 @@
1
1
  module SqlLexer
2
- VERSION = "0.0.1"
2
+ VERSION = "0.0.3"
3
3
  end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: skylight
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.2.2
4
+ version: 0.2.3
5
5
  platform: ruby
6
6
  authors:
7
7
  - Tilde, Inc.
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2013-12-10 00:00:00.000000000 Z
11
+ date: 2013-12-21 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: activesupport