skylight 0.1.8 → 0.2.0.beta.1

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: 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