skylight 0.1.8 → 0.2.0.beta.1

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: cdeb1ad6b563b4b7a487b38421575943337819a9
4
- data.tar.gz: 593f06bddf3ffa1ac6dca9544ce84bcecf7680fa
3
+ metadata.gz: b64f9d0eb45e48301e2b937366229e63bc339af3
4
+ data.tar.gz: 39656e4d86ac0d4f3496101643105fba58fdf4a6
5
5
  SHA512:
6
- metadata.gz: 220a2e262e02e983112cc177c2cae80b11fd8a7d1c0f0235656fc250ff361db4017b3053be6d6d1ffe6c6c2e5316c1f88566764c6a2a2edb171368f095c6f734
7
- data.tar.gz: 3f5fd68ec9ec55c2e2703aa399b4e376b6b75bf70b66fdd058add5c9d5db1ce00428c4df3e721306bd6499e39d487e1363ddf397a7e4ef88ccc0e0621393065f
6
+ metadata.gz: 3fe73155548f5bca71d6fab4049809fe354c8c6b205f9e26001595dd6454920a3c3fdbcab7b224a6069336ab25c3d38090f19133ba4665b523d63ca120ca0b6f
7
+ data.tar.gz: f6f4ab23ae9c2771d2eb702725e54dce515885319e13b442e02db549af99a349014cb45b17e370125e79b2f412005cd239e919c4ab266839cba9ab8aca49e1ce
data/lib/skylight.rb CHANGED
@@ -49,6 +49,7 @@ module Skylight
49
49
  class WorkerStateError < RuntimeError; end
50
50
  class ConfigError < RuntimeError; end
51
51
  class TraceError < RuntimeError; end
52
+ class SerializeError < RuntimeError; end
52
53
 
53
54
  TIERS = %w(
54
55
  api
@@ -14,7 +14,7 @@ module Skylight
14
14
  hostname = nil if hostname == ''
15
15
  end
16
16
 
17
- hostname || SecureRandom.uuid
17
+ hostname || "gen-#{SecureRandom.uuid}"
18
18
  end
19
19
 
20
20
  # Map environment variable keys with Skylight configuration keys
@@ -1,18 +1,24 @@
1
1
  require 'thread'
2
+ require 'set'
2
3
 
3
4
  module Skylight
4
5
  class Instrumenter
5
6
  KEY = :__skylight_current_trace
6
7
  LOCK = Mutex.new
8
+ DESC_LOCK = Mutex.new
9
+
10
+ TOO_MANY_UNIQUES = "<too many unique descriptions>"
7
11
 
8
12
  include Util::Logging
9
13
 
10
- def self.current_trace
11
- Thread.current[KEY]
12
- end
14
+ class TraceInfo
15
+ def current
16
+ Thread.current[KEY]
17
+ end
13
18
 
14
- def self.current_trace=(trace)
15
- Thread.current[KEY] = trace
19
+ def current=(trace)
20
+ Thread.current[KEY] = trace
21
+ end
16
22
  end
17
23
 
18
24
  def self.instance
@@ -36,7 +42,7 @@ module Skylight
36
42
  end
37
43
  end
38
44
 
39
- attr_reader :config, :gc
45
+ attr_reader :config, :gc, :trace_info
40
46
 
41
47
  def initialize(config)
42
48
  if Hash === config
@@ -46,7 +52,18 @@ module Skylight
46
52
  @gc = config.gc
47
53
  @config = config
48
54
  @worker = config.worker.build
49
- @subscriber = Subscriber.new(config)
55
+ @subscriber = Subscriber.new(config, self)
56
+
57
+ @trace_info = @config[:trace_info] || TraceInfo.new
58
+ @descriptions = Hash.new { |h,k| h[k] = Set.new }
59
+ end
60
+
61
+ def current_trace
62
+ @trace_info.current
63
+ end
64
+
65
+ def current_trace=(trace)
66
+ @trace_info.current = trace
50
67
  end
51
68
 
52
69
  def start!
@@ -72,7 +89,7 @@ module Skylight
72
89
 
73
90
  def trace(endpoint, cat, *args)
74
91
  # If a trace is already in progress, continue with that one
75
- if trace = Instrumenter.current_trace
92
+ if trace = @trace_info.current
76
93
  t { "already tracing" }
77
94
  return yield(trace) if block_given?
78
95
  return trace
@@ -86,20 +103,20 @@ module Skylight
86
103
  return
87
104
  end
88
105
 
89
- Instrumenter.current_trace = trace
106
+ @trace_info.current = trace
90
107
  return trace unless block_given?
91
108
 
92
109
  begin
93
110
  yield trace
94
111
 
95
112
  ensure
96
- Instrumenter.current_trace = nil
113
+ @trace_info.current = nil
97
114
  trace.submit
98
115
  end
99
116
  end
100
117
 
101
118
  def instrument(cat, *args)
102
- unless trace = Instrumenter.current_trace
119
+ unless trace = @trace_info.current
103
120
  return yield if block_given?
104
121
  return
105
122
  end
@@ -128,6 +145,31 @@ module Skylight
128
145
  end
129
146
  end
130
147
 
148
+ def limited_description(description)
149
+ endpoint = @trace_info.current.endpoint
150
+
151
+ DESC_LOCK.synchronize do
152
+ set = @descriptions[endpoint]
153
+
154
+ if set.size >= 100
155
+ return TOO_MANY_UNIQUES if set.size >= 100
156
+ end
157
+
158
+ set << description
159
+ description
160
+ end
161
+ end
162
+
163
+ def error(reason, body)
164
+ t { fmt "processing error; reason=%s; body=%s", reason, body }
165
+
166
+ message = Skylight::Messages::Error.new(reason: reason, body: body)
167
+
168
+ unless @worker.submit(message)
169
+ warn "failed to submit error to worker"
170
+ end
171
+ end
172
+
131
173
  def process(trace)
132
174
  t { fmt "processing trace; spans=%d; duration=%d",
133
175
  trace.spans.length, trace.spans[-1].duration }
@@ -15,5 +15,6 @@ module Skylight
15
15
  require 'skylight/messages/endpoint'
16
16
  require 'skylight/messages/batch'
17
17
  require 'skylight/messages/hello'
18
+ require 'skylight/messages/error'
18
19
  end
19
20
  end
@@ -0,0 +1,10 @@
1
+ require 'skylight/messages/base'
2
+
3
+ module Skylight
4
+ module Messages
5
+ class Error < Base
6
+ required :reason, :string, 1
7
+ required :body, :string, 2
8
+ end
9
+ end
10
+ end
@@ -1,5 +1,71 @@
1
1
  module Skylight
2
2
  module Messages
3
+ class AnnotationBuilder
4
+ def self.build(object)
5
+ new(object).build
6
+ end
7
+
8
+ def initialize(object)
9
+ @object = object
10
+ end
11
+
12
+ def build
13
+ build_nested(@object)
14
+ end
15
+
16
+ private
17
+ def build_nested(object)
18
+ case object
19
+ when Hash
20
+ build_hash(object)
21
+ when Array
22
+ build_array(object)
23
+ end
24
+ end
25
+
26
+ def build_hash(object)
27
+ object.map do |key, value|
28
+ build_annotation(value, key)
29
+ end
30
+ end
31
+
32
+ def build_array(array)
33
+ array.map do |value|
34
+ build_annotation(value)
35
+ end
36
+ end
37
+
38
+ def build_annotation(value, key=nil)
39
+ Annotation.new.tap do |annotation|
40
+ annotation.key = key.to_s if key
41
+ annotation[classify(value)] = build_value(value)
42
+ end
43
+ end
44
+
45
+ def build_value(value)
46
+ nested?(value) ? build_nested(value) : value
47
+ end
48
+
49
+ def nested?(value)
50
+ value.is_a?(Array) || value.is_a?(Hash)
51
+ end
52
+
53
+ def classify(value)
54
+ case value
55
+ when String
56
+ :string
57
+ when Integer
58
+ :int
59
+ when Numeric
60
+ :double
61
+ when Hash, Array
62
+ :nested
63
+ else
64
+ raise Skylight::SerializeError.new("Annotation values must be Strings or Numeric. You passed #{value.inspect} in #{@object.inspect}")
65
+ end
66
+ end
67
+ end
68
+
3
69
  class Span
4
70
  include Beefcake::Message
5
71
 
@@ -32,13 +98,15 @@ module Skylight
32
98
  attr_accessor :children
33
99
 
34
100
  def initialize(trace, time, started_at, cat, title, desc, annot)
35
- @trace = trace
36
- @built = false
37
- @time = time
38
- @started_at = started_at
39
- @category = cat.to_s
40
- @children = 0
41
- self.title = title
101
+ @trace = trace
102
+ @built = false
103
+ @time = time
104
+ @started_at = started_at
105
+ @category = cat.to_s
106
+ @children = 0
107
+ @annotations = annot
108
+
109
+ self.title = title
42
110
  self.description = desc
43
111
  end
44
112
 
@@ -87,7 +155,9 @@ module Skylight
87
155
  private
88
156
 
89
157
  def to_annotations(val)
90
- [] # TODO: Implement
158
+ return nil if !val || val.empty?
159
+
160
+ AnnotationBuilder.build(val)
91
161
  end
92
162
 
93
163
  end
@@ -51,6 +51,8 @@ module Skylight
51
51
  desc = args.shift
52
52
  now = adjust_for_skew(Util::Clock.micros)
53
53
 
54
+ desc = @instrumenter.limited_description(desc)
55
+
54
56
  sp = span(now - gc_time, cat, title, desc, annot)
55
57
  inc_children
56
58
  @spans << sp.build(0)
@@ -59,11 +61,13 @@ module Skylight
59
61
  end
60
62
 
61
63
  def instrument(cat, *args)
62
- annot = args.pop if Hash === args
64
+ annot = args.pop if Hash === args.last
63
65
  title = args.shift
64
66
  desc = args.shift
65
67
  now = adjust_for_skew(Util::Clock.micros)
66
68
 
69
+ desc = @instrumenter.limited_description(desc)
70
+
67
71
  start(now - gc_time, cat, title, desc, annot)
68
72
  end
69
73
 
@@ -73,8 +77,8 @@ module Skylight
73
77
  end
74
78
 
75
79
  def release
76
- return unless Instrumenter.current_trace == self
77
- Instrumenter.current_trace = nil
80
+ return unless @instrumenter.current_trace == self
81
+ @instrumenter.current_trace = nil
78
82
  end
79
83
 
80
84
  def submit
@@ -5,7 +5,7 @@ module Skylight
5
5
 
6
6
  def normalize(trace, name, payload)
7
7
  trace.endpoint = controller_action(payload)
8
- [ "app.controller.request", trace.endpoint, nil, payload ]
8
+ [ "app.controller.request", trace.endpoint, nil, normalize_payload(payload) ]
9
9
  end
10
10
 
11
11
  private
@@ -13,6 +13,17 @@ module Skylight
13
13
  def controller_action(payload)
14
14
  "#{payload[:controller]}##{payload[:action]}"
15
15
  end
16
+
17
+ def normalize_payload(payload)
18
+ normalized = {}
19
+
20
+ payload.each do |key, value|
21
+ value = value.inspect unless value.is_a?(String) || value.is_a?(Numeric)
22
+ normalized[key] = value
23
+ end
24
+
25
+ normalized
26
+ end
16
27
  end
17
28
  end
18
29
  end
@@ -7,7 +7,7 @@ module Skylight
7
7
  normalize_render(
8
8
  "view.render.template",
9
9
  payload,
10
- partial: true)
10
+ partial: 1)
11
11
  end
12
12
  end
13
13
  end
@@ -7,7 +7,7 @@ module Skylight
7
7
  normalize_render(
8
8
  "view.render.template",
9
9
  payload,
10
- partial: false)
10
+ partial: 0)
11
11
  end
12
12
  end
13
13
  end
@@ -1,3 +1,6 @@
1
+ require "sql_lexer"
2
+ require "json"
3
+
1
4
  module Skylight
2
5
  module Normalizers
3
6
  class SQL < Normalizer
@@ -9,16 +12,39 @@ module Skylight
9
12
  return :skip
10
13
  else
11
14
  name = "db.sql.query"
12
- title = payload[:name]
15
+ title = payload[:name] || "SQL"
13
16
  end
14
17
 
15
- binds = payload[:binds]
18
+ if payload[:binds].empty?
19
+ extracted_title, payload[:sql], binds, error = extract_binds(payload)
20
+ else
21
+ extracted_title, _, _, error = extract_binds(payload)
22
+ binds = payload[:binds].map { |col, val| val.inspect }
23
+ end
24
+
25
+ title = extracted_title if extracted_title
26
+
16
27
 
17
- annotations = {
18
- sql: payload[:sql],
19
- binds: binds ? binds.map(&:last) : [] }
28
+ if payload[:sql]
29
+ annotations = {
30
+ sql: payload[:sql],
31
+ binds: binds,
32
+ }
33
+ else
34
+ annotations = {
35
+ skylight_error: error
36
+ }
37
+ end
38
+
39
+ [ name, title, payload[:sql], annotations ]
40
+ end
20
41
 
21
- [ name, title, nil, annotations ]
42
+ private
43
+ def extract_binds(payload)
44
+ title, sql, binds = SqlLexer::Lexer.bindify(payload[:sql])
45
+ [ title, sql, binds, nil ]
46
+ rescue
47
+ [ nil, nil, nil, ["sql_parse", payload[:sql]] ]
22
48
  end
23
49
  end
24
50
  end
@@ -24,6 +24,10 @@ module Skylight
24
24
 
25
25
  private
26
26
 
27
+ def existent_paths(paths)
28
+ paths.respond_to?(:existent) ? paths.existent : paths.select { |f| File.exists?(f) }
29
+ end
30
+
27
31
  def load_skylight_config(app)
28
32
  path = config_path(app)
29
33
  path = nil unless File.exist?(path)
@@ -39,7 +43,7 @@ module Skylight
39
43
  configure_logging(config, app)
40
44
 
41
45
  config['agent.sockfile_path'] = tmp
42
- config['normalizers.render.view_paths'] = app.config.paths["app/views"].existent
46
+ config['normalizers.render.view_paths'] = existent_paths(app.config.paths["app/views"])
43
47
  config.validate!
44
48
  config
45
49
 
@@ -4,10 +4,11 @@ module Skylight
4
4
 
5
5
  attr_reader :config
6
6
 
7
- def initialize(config)
8
- @config = config
9
- @subscriber = nil
10
- @normalizers = Normalizers.build(config)
7
+ def initialize(config, instrumenter)
8
+ @config = config
9
+ @subscriber = nil
10
+ @normalizers = Normalizers.build(config)
11
+ @instrumenter = instrumenter
11
12
  end
12
13
 
13
14
  def register!
@@ -35,23 +36,32 @@ module Skylight
35
36
  end
36
37
 
37
38
  def start(name, id, payload)
38
- return unless trace = Instrumenter.current_trace
39
+ return unless trace = @instrumenter.current_trace
39
40
 
40
41
  cat, title, desc, annot = normalize(trace, name, payload)
41
42
 
43
+ if cat != :skip && error = annot[:skylight_error]
44
+ @instrumenter.error(*error)
45
+ end
46
+
42
47
  unless cat == :skip
43
48
  span = trace.instrument(cat, title, desc, annot)
44
49
  end
45
50
 
46
51
  trace.notifications << Notification.new(name, span)
47
-
48
52
  rescue Exception => e
49
53
  error "Subscriber#start error; msg=%s", e.message
54
+ debug "trace=%s", trace.inspect
55
+ debug "in: name=%s", name.inspect
56
+ debug "in: payload=%s", payload.inspect
57
+ debug "out: cat=%s, title=%s, desc=%s", cat.inspect, name.inspect, desc.inspect
58
+ debug "out: annot=%s", annot.inspect
59
+ t { e.backtrace.join("\n") }
50
60
  nil
51
61
  end
52
62
 
53
63
  def finish(name, id, payload)
54
- return unless trace = Instrumenter.current_trace
64
+ return unless trace = @instrumenter.current_trace
55
65
 
56
66
  while curr = trace.notifications.pop
57
67
  if curr.name == name
@@ -62,6 +72,10 @@ module Skylight
62
72
 
63
73
  rescue Exception => e
64
74
  error "Subscriber#finish error; msg=%s", e.message
75
+ debug "trace=%s", trace.inspect
76
+ debug "in: name=%s", name.inspect
77
+ debug "in: payload=%s", payload.inspect
78
+ t { e.backtrace.join("\n") }
65
79
  nil
66
80
  end
67
81
 
@@ -1,4 +1,4 @@
1
1
  module Skylight
2
- VERSION = '0.1.8'
2
+ VERSION = '0.2.0.beta.1'
3
3
  end
4
4
 
@@ -49,9 +49,12 @@ module Skylight
49
49
 
50
50
  return true unless msg
51
51
 
52
- if Messages::Trace === msg
52
+ case msg
53
+ when Messages::Trace
53
54
  t { fmt "collector received trace" }
54
55
  @batch.push(msg)
56
+ when Messages::Error
57
+ send_error(msg)
55
58
  else
56
59
  debug "Received unknown message; class=%s", msg.class.to_s
57
60
  end
@@ -61,6 +64,22 @@ module Skylight
61
64
 
62
65
  private
63
66
 
67
+ def send_error(msg)
68
+ res = @http_auth.post("/agent/error?hostname=#{escape(config[:'hostname'])}", reason: msg.reason, body: msg.body)
69
+
70
+ unless res.success?
71
+ if (400..499).include? @res.status
72
+ warn "error wasn't sent successfully; status=%s", res.status
73
+ end
74
+
75
+ warn "could not fetch report session token; status=%s", res.status
76
+ return
77
+ end
78
+ rescue Exception => e
79
+ error "exception; msg=%s; class=%s", e.message, e.class
80
+ t { e.backtrace.join("\n") }
81
+ end
82
+
64
83
  def finish
65
84
  t { fmt "collector finishing up" }
66
85
 
@@ -90,6 +109,11 @@ module Skylight
90
109
  res = @http_auth.get("/agent/authenticate?hostname=#{escape(config[:'hostname'])}")
91
110
 
92
111
  unless res.success?
112
+ if (400..499).include? @res.status
113
+ warn "token request rejected; status=%s", res.status
114
+ @http_report = nil
115
+ end
116
+
93
117
  warn "could not fetch report session token; status=%s", res.status
94
118
  return
95
119
  end
@@ -98,12 +122,12 @@ module Skylight
98
122
  tok = tok['token'] if tok
99
123
 
100
124
  if tok
101
- @refresh_at = now + 600
125
+ @refresh_at = now + 30
102
126
  @http_report = Util::HTTP.new(config, :report)
103
127
  @http_report.authentication = tok
104
128
  else
105
129
  if @http_report
106
- @refresh_at = now + 60
130
+ @refresh_at = now + 30
107
131
  end
108
132
  warn "server did not return a session token"
109
133
  end
@@ -21,7 +21,6 @@ module Skylight
21
21
  :sockfile_path
22
22
 
23
23
  def initialize(config, lockfile, srv, lockfile_path)
24
-
25
24
  unless lockfile && srv
26
25
  raise ArgumentError, "lockfile and unix domain server socket are required"
27
26
  end
@@ -214,8 +213,8 @@ module Skylight
214
213
  info "newer version of agent deployed - restarting; curr=%s; new=%s", VERSION, msg.version
215
214
  reload(msg)
216
215
  end
217
- when Messages::Trace
218
- t { "received trace" }
216
+ when Messages::Base
217
+ t { "received message" }
219
218
  @collector.submit(msg)
220
219
  when :unknown
221
220
  debug "received unknown message"
data/lib/sql_lexer.rb ADDED
@@ -0,0 +1,6 @@
1
+ require "sql_lexer/version"
2
+ require "sql_lexer/lexer"
3
+
4
+ module SqlLexer
5
+ # Your code goes here...
6
+ end
@@ -0,0 +1,259 @@
1
+ require "strscan"
2
+
3
+ module SqlLexer
4
+ class Lexer
5
+ # SQL identifiers and key words must begin with a letter (a-z, but also
6
+ # letters with diacritical marks and non-Latin letters) or an underscore
7
+ # (_). Subsequent characters in an identifier or key word can be letters,
8
+ # underscores, digits (0-9), or dollar signs ($). Note that dollar signs
9
+ # are not allowed in identifiers according to the letter of the SQL
10
+ # standard, so their use might render applications less portable. The SQL
11
+ # standard will not define a key word that contains digits or starts or
12
+ # ends with an underscore, so identifiers of this form are safe against
13
+ # possible conflict with future extensions of the standard.
14
+ StartID = %q<\p{Alpha}_>
15
+ PartID = %q<\p{Alnum}_$>
16
+ OpPart = %q<\+|\-(?!-)|\*|/(?!\*)|\<|\>|=|~|!|@|#|%|\^|&|\||\?|\.|,|\(|\)>
17
+ WS = %q< \t\r\n>
18
+ OptWS = %Q<[#{WS}]*>
19
+
20
+ InOp = %q<IN>
21
+ SpecialOps = %Q<#{InOp}>
22
+
23
+ StartQuotedID = %Q<">
24
+ StartTickedID = %Q<`>
25
+ StartString = %Q<'>
26
+ StartDigit = %q<[\p{Digit}\.]>
27
+ StartBind = %Q<#{StartString}|#{StartDigit}|#{SpecialOps}>
28
+ StartNonBind = %Q<#{StartQuotedID}|#{StartTickedID}|\\$(?=\\p{Digit})>
29
+ TableNext = %Q<(#{OptWS}((?=#{StartQuotedID})|(?=#{StartTickedID}))|[#{WS}]+(?=[#{StartID}]))>
30
+ StartAnyId = %Q<"#{StartID}>
31
+ Placeholder = %q<\$\p{Digit}+>
32
+
33
+ AfterID = %Q<[#{WS};#{StartNonBind}]|(?:#{OpPart})|$>
34
+ ID = %Q<[#{StartID}][#{PartID}]*(?=#{AfterID})>
35
+ AfterOp = %Q<[#{WS}]|[#{StartAnyId}]|[#{StartBind}]|(#{StartNonBind})|$>
36
+ Op = %Q<(?:#{OpPart})+(?=#{AfterOp})>
37
+ QuotedID = %Q<#{StartQuotedID}(?:[^"]|"")*">
38
+ TickedID = %Q<#{StartTickedID}(?:[^`]|``)*`>
39
+ NonBind = %Q<#{ID}|#{Op}|#{QuotedID}|#{TickedID}|#{Placeholder}>
40
+ QuotedTable = %Q<#{TickedID}|#{QuotedID}>
41
+
42
+ String = %Q<#{StartString}(?:[^']|'')*'>
43
+
44
+ Digits = %q<\p{Digit}+>
45
+ OptDigits = %q<\p{Digit}*>
46
+ Exponent = %Q<e[+\-]?#{Digits}>
47
+ OptExponent = %Q<(?:#{Exponent})?>
48
+ HeadDecimal = %Q<#{Digits}\\.#{OptDigits}#{OptExponent}>
49
+ TailDecimal = %Q<#{OptDigits}\\.#{Digits}#{OptExponent}>
50
+ ExpDecimal = %Q<#{Digits}#{Exponent}>
51
+
52
+ Number = %Q<#{HeadDecimal}|#{TailDecimal}|#{ExpDecimal}|#{Digits}>
53
+
54
+ TkWS = %r<[#{WS}]+>
55
+ TkOptWS = %r<[#{WS}]*>
56
+ TkOp = %r<[#{OpPart}]>
57
+ TkPlaceholder = %r<#{Placeholder}>
58
+ TkNonBind = %r<#{NonBind}>
59
+ TkQuotedTable = %r<#{QuotedTable}>i
60
+ TkUpdateTable = %r<UPDATE#{TableNext}>i
61
+ TkInsertTable = %r<INSERT[#{WS}]+INTO#{TableNext}>i
62
+ TkDeleteTable = %r<DELETE[#{WS}]+FROM#{TableNext}>i
63
+ TkFromTable = %r<FROM#{TableNext}>i
64
+ TkID = %r<#{ID}>
65
+ TkEnd = %r<;?[#{WS}]*>
66
+ TkBind = %r<#{String}|#{Number}>
67
+ TkIn = %r<#{InOp}>i
68
+ TkSpecialOp = %r<#{SpecialOps}>i
69
+
70
+ STATE_HANDLERS = {
71
+ begin: :process_begin,
72
+ first_token: :process_first_token,
73
+ tokens: :process_tokens,
74
+ bind: :process_bind,
75
+ non_bind: :process_non_bind,
76
+ table_name: :process_table_name,
77
+ end: :process_end,
78
+ special: :process_special,
79
+ in: :process_in
80
+ }
81
+
82
+ def self.bindify(string)
83
+ new(string).tap do |scanner|
84
+ scanner.process
85
+ return scanner.title, scanner.output, scanner.binds
86
+ end
87
+ end
88
+
89
+ attr_reader :output, :binds, :title
90
+
91
+ def initialize(string)
92
+ @scanner = StringScanner.new(string)
93
+ @state = :begin
94
+ @input = string
95
+ @output = string.dup
96
+ @binds = []
97
+ end
98
+
99
+ def process
100
+ @operation = @table = @title = nil
101
+
102
+ while @state
103
+ if ENV["DEBUG"]
104
+ p @state
105
+ p @scanner
106
+ end
107
+
108
+ send STATE_HANDLERS[@state]
109
+ end
110
+
111
+ pos = 0
112
+ removed = 0
113
+ extracted_binds = Array.new(@binds.size / 2)
114
+
115
+ if @operation && @table
116
+ table = @input[@table[0], @table[1] - @table[0]]
117
+ @title = "#{@operation} #{table}"
118
+ end
119
+
120
+ while pos < @binds.size
121
+ slice = @output.slice!(@binds[pos] - removed, @binds[pos+1])
122
+ @output.insert(@binds[pos] - removed, '?')
123
+ extracted_binds[pos/2] = slice
124
+ removed += slice.size - 1
125
+ pos += 2
126
+ end
127
+
128
+ @binds = extracted_binds
129
+ end
130
+
131
+ def process_begin
132
+ @scanner.scan(TkOptWS)
133
+ @state = :first_token
134
+ end
135
+
136
+ def process_first_token
137
+ if @scanner.skip(/SELECT\s+/i)
138
+ @operation = :"SELECT FROM"
139
+ @state = :tokens
140
+ return
141
+ end
142
+
143
+ if @scanner.skip(TkUpdateTable)
144
+ @operation = :UPDATE
145
+ elsif @scanner.skip(TkInsertTable)
146
+ @operation = :"INSERT INTO"
147
+ elsif @scanner.skip(TkDeleteTable)
148
+ @operation = :"DELETE FROM"
149
+ end
150
+
151
+ @state = :table_name
152
+ end
153
+
154
+ def process_table_name
155
+ pos = @scanner.pos
156
+
157
+ if @scanner.skip(TkQuotedTable)
158
+ @table = [pos + 1, @scanner.pos - 1]
159
+ elsif @scanner.skip(TkID)
160
+ @table = [pos, @scanner.pos]
161
+ end
162
+
163
+ @state = :tokens
164
+ end
165
+
166
+ def process_tokens
167
+ @scanner.skip(TkOptWS)
168
+
169
+ if @operation == :"SELECT FROM" && !@table && @scanner.skip(TkFromTable)
170
+ @state = :table_name
171
+ elsif @scanner.match?(TkSpecialOp)
172
+ @state = :special
173
+ elsif @scanner.match?(TkBind)
174
+ @state = :bind
175
+ elsif @scanner.match?(TkNonBind)
176
+ @state = :non_bind
177
+ else
178
+ @state = :end
179
+ end
180
+ end
181
+
182
+ def process_special
183
+ if @scanner.skip(TkIn)
184
+ @scanner.skip(TkOptWS)
185
+ @scanner.skip(/\(/)
186
+ @state = :in
187
+ end
188
+ end
189
+
190
+ def process_in
191
+ nest = 1
192
+ iterations = 0
193
+
194
+ @skip_binds = true
195
+ pos = @scanner.pos - 1
196
+
197
+ while nest > 0
198
+ iterations += 1
199
+
200
+ if iterations > 10_000
201
+ raise "The SQL '#{@scanner.string}' could not be parsed because of too many iterations in IN"
202
+ end
203
+
204
+ if ENV["DEBUG"]
205
+ p @state
206
+ p @scanner
207
+ p nest
208
+ end
209
+
210
+ if @scanner.skip(/\(/)
211
+ nest += 1
212
+ process_tokens
213
+ elsif @scanner.skip(/\)/)
214
+ nest -= 1
215
+ break if nest.zero?
216
+ process_tokens
217
+ else
218
+ process_tokens
219
+ end
220
+
221
+ send STATE_HANDLERS[@state]
222
+ end
223
+
224
+ @binds << pos
225
+ @binds << @scanner.pos - pos
226
+
227
+ @skip_binds = false
228
+
229
+ @state = :tokens
230
+ end
231
+
232
+ def process_non_bind
233
+ @scanner.skip(TkNonBind)
234
+ @state = :tokens
235
+ end
236
+
237
+ def process_bind
238
+ pos = @scanner.pos
239
+ size = @scanner.skip(TkBind)
240
+
241
+ unless @skip_binds
242
+ @binds << pos
243
+ @binds << size
244
+ end
245
+
246
+ @state = :tokens
247
+ end
248
+
249
+ def process_end
250
+ @scanner.skip(TkEnd)
251
+
252
+ unless @scanner.eos?
253
+ raise "The SQL '#{@scanner.string}' could not be parsed"
254
+ end
255
+
256
+ @state = nil
257
+ end
258
+ end
259
+ end
@@ -0,0 +1,3 @@
1
+ module SqlLexer
2
+ VERSION = "0.0.1"
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.1.8
4
+ version: 0.2.0.beta.1
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-07-19 00:00:00.000000000 Z
11
+ date: 2013-10-30 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: activesupport
@@ -46,6 +46,7 @@ files:
46
46
  - lib/skylight/messages/base.rb
47
47
  - lib/skylight/messages/batch.rb
48
48
  - lib/skylight/messages/endpoint.rb
49
+ - lib/skylight/messages/error.rb
49
50
  - lib/skylight/messages/event.rb
50
51
  - lib/skylight/messages/hello.rb
51
52
  - lib/skylight/messages/span.rb
@@ -127,6 +128,9 @@ files:
127
128
  - lib/skylight/worker/embedded.rb
128
129
  - lib/skylight/worker/server.rb
129
130
  - lib/skylight/worker/standalone.rb
131
+ - lib/sql_lexer.rb
132
+ - lib/sql_lexer/lexer.rb
133
+ - lib/sql_lexer/version.rb
130
134
  - CHANGELOG.md
131
135
  - README.md
132
136
  - bin/skylight
@@ -144,9 +148,9 @@ required_ruby_version: !ruby/object:Gem::Requirement
144
148
  version: 1.9.2
145
149
  required_rubygems_version: !ruby/object:Gem::Requirement
146
150
  requirements:
147
- - - '>='
151
+ - - '>'
148
152
  - !ruby/object:Gem::Version
149
- version: '0'
153
+ version: 1.3.1
150
154
  requirements: []
151
155
  rubyforge_project:
152
156
  rubygems_version: 2.0.3