lines 0.1.27 → 0.2.0

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,15 +1,15 @@
1
1
  ---
2
2
  !binary "U0hBMQ==":
3
3
  metadata.gz: !binary |-
4
- OTM0ODk2MzlkMTQ3YmJhMTc2MmI4MGMzNzYzNTQyMTFjNmQ5OTYyZQ==
4
+ ZGM0YmFkODI1ZDgwOTA1NTQ1Y2JmZjM5ZDE2ZjIxMzAxM2JhNWZhZg==
5
5
  data.tar.gz: !binary |-
6
- OTA4YzE1ZTJkMjQwZDM0NmRiOGVlMDY1MzFkMmIxYmJhMzYxNzlmZA==
6
+ YzY3M2FhMGEyNmExZGEyODZiMDIyMDgzOWQ4YTQ4NThiMzM2NDI3NQ==
7
7
  !binary "U0hBNTEy":
8
8
  metadata.gz: !binary |-
9
- YWZlYzZmYzdhNjBjOTIwNGU2MjYyMjFmMjNkNGNjYzE1NmI2NTk2NzJlOGYx
10
- MGRhNmU2ZjYxNWY3MGJlMWExMjVjYzEwMTBiZGU0MGFlMWIyNzEyNjE0NjQ4
11
- YzA0Y2QwZTc4NmI4N2E3YmI4OGQwZDY4Y2E4OTI5NmI2N2FmNzY=
9
+ NTI3YzJhY2M5YjMzZDE2NWI3NjdjYmE0MDZjOWU0MDg1MjY0ZTY2NmYzYThh
10
+ ZjkxNTQ4ZjZjMzQ0OGRiMTZmNTc5ZDhmYjI3ODIwMDg4MDg0NTFmYWE4OWU3
11
+ MmUyYzgxOTU0MjQ0N2NhNDFmNThkZmU3Y2NjOWMyZWUxMWIwMmU=
12
12
  data.tar.gz: !binary |-
13
- NWFkZGFkNjY5OWE4NmViZWEzOWNhNzllNjZiZTg1NzI2YmFjNzRmOTYyNGE0
14
- OTM4ZDA0YTc1ZmM5MzdmYzdlY2I0YzMwOTVhNDE0YTBjYjk5YzM0MjVhNGEw
15
- NTUxOWIyOTkzOGExNDliZjY1ZTI0NTUyYzVlYjlkNTY3ZWE5Yzg=
13
+ Njc3MjM5ZjJmMmY0MWE4OWM1ZmFlYmUyNzdlMmQ0OWFmZTVmZTllZjc0YWRk
14
+ ZDczMjM4YmEwYTMwYTU1OTU5Y2E1OWIwNjAzMGNiYTc1NTdiNGJhNWUzZjE5
15
+ ZjhiNDlhYWU1ZGM4MTBmZTQ3YmY4ZmQyNjMxOWU5NDMyZjZiMWU=
@@ -1,4 +1,21 @@
1
1
 
2
+ 0.2.0 / 2013-07-15
3
+ ==================
4
+
5
+ * Use benchmark-ips for benchs
6
+ * Fixes serialization of Date objects
7
+ * Lines now outputs to $stderr by default.
8
+ * Lines.use resets the global context.
9
+ * Improved the doc
10
+ * Lines.log now returns nil instead of the logged object.
11
+ * Support parsing lines that end with \r\n or spaces
12
+ * Add Lines.load and Lines.dump for JSON-like functionality
13
+ * Introduced a hand-written parser that performs 200x faster
14
+ * Differentiate units with a : sign to ensure their parsability
15
+ * Escape strings that contain an equal sign
16
+ * Change the default max_depth from 3 to 4
17
+ * Make sure ActiveRecord's log subscriber is loaded
18
+
2
19
  0.1.27 / 2013-07-10
3
20
  ===================
4
21
 
data/Gemfile CHANGED
@@ -1,10 +1,10 @@
1
1
  source 'http://rubygems.org'
2
2
  gemspec
3
3
  group :development do
4
- gem 'parslet'
5
4
  gem 'rdoc'
6
5
  end
7
6
  group :test do
8
- gem 'rspec'
7
+ gem 'benchmark-ips'
9
8
  gem 'rake'
9
+ gem 'rspec'
10
10
  end
data/README.md CHANGED
@@ -6,10 +6,85 @@ Status](https://travis-ci.org/zimbatm/lines-ruby.png)](https://travis-ci.org/zim
6
6
  An oppinionated logging library that implement the
7
7
  [lines](https://github.com/zimbatm/lines) format.
8
8
 
9
- Status: work in progress
9
+ * Log everything in development AND production.
10
+ * Logs should be easy to read, grep and parse.
11
+ * Logging something should never fail.
12
+ * Let the system handle the storage. Write to syslog or STDERR.
13
+ * No log levels necessary. Just log whatever you want.
10
14
 
11
- See also
15
+ STATUS: WORK IN PROGRESS
16
+ ========================
17
+
18
+ Doc is still scarce so it's quite hard to get started. I think reading the
19
+ lib/lines.rb should give a good idea of the capabilities.
20
+
21
+ Lines.id is a unique ID generator that seems quite handy but I'm not sure if
22
+ it should be part of the lib or not.
23
+
24
+ It would be nice to expose a method that resolves a context into a hash. It's
25
+ useful to share the context with other tools like an error reporter. Btw,
26
+ Sentry/Raven is great.
27
+
28
+ There is a parser in the lib but no credible direct consumption path.
29
+
30
+ Quick intro
31
+ -----------
32
+
33
+ ```ruby
34
+ require 'lines'
35
+
36
+ # Setups the outputs. IO and Syslog are supported.
37
+ Lines.use($stdout, Syslog)
38
+
39
+ # All lines will be prefixed by the global context
40
+ Lines.global['at'] = proc{ Time.now }
41
+
42
+ # First example
43
+ Lines.log(foo: 'bar') # logs: at=2013-07-14T14:19:28Z foo=bar
44
+
45
+ # If not a hash, the argument is transformed. A second argument is accepted as
46
+ # a hash
47
+ Lines.log("Hey", count: 3) # logs: at=2013-07-14T14:19:28Z msg=Hey count=3
48
+
49
+ # You can also keep a context
50
+ class MyClass < ActiveRecord::Base
51
+ attr_reader :lines
52
+ def initialize
53
+ @lines = Lines.context(my_class_id: self.id)
54
+ end
55
+
56
+ def do_something
57
+ lines.log("Something happened")
58
+ # logs: at=2013-07-14T14:19:28Z msg='Something happeend' my_class_id: 2324
59
+ end
60
+ end
61
+ ```
62
+
63
+ Features
12
64
  --------
13
65
 
66
+ * Simple to use
67
+ * Thread safe (if the IO#write is)
68
+ * Designed to not raise exceptions (unless it's an IO issue)
69
+ * Lines.logger is a backward-compatible Logger in case you want to retrofit
70
+ * require "lines/active_record" for sane ActiveRecord logs
71
+ * "lines/rack_logger" is a logging middleware for Rack
72
+ * Lines.load and Lines.dump to parse and generate 'lines'
73
+
74
+ There's also a fork of lograge that you can use with Rails. See
75
+ https://github.com/zimbatm/lograge/tree/lines-output
76
+
77
+ Known issues
78
+ ------------
79
+
80
+ Syslog seems to truncate lines longer than 2056 chars and Lines makes if very
81
+ easy to put too much data.
82
+
83
+ Lines logging speed is reasonable but it could be faster. It writes at around
84
+ 5000 lines per second to Syslog on my machine.
85
+
86
+ Inspired by
87
+ -----------
88
+
14
89
  * Scrolls : https://github.com/asenchi/scrolls
15
90
  * Lograge : https://github.com/roidrage/lograge
@@ -1,19 +1,17 @@
1
1
  require 'date'
2
2
  require 'time'
3
- require 'forwardable'
4
3
 
5
- # Lines is an opinionated structured log format and a library
6
- # inspired by Slogger.
4
+ # Lines is an opinionated structured log format and a library.
7
5
  #
8
- # Don't use log levels. They limit the reasoning of the developer.
9
6
  # Log everything in development AND production.
10
7
  # Logs should be easy to read, grep and parse.
11
8
  # Logging something should never fail.
12
- # Use syslog.
9
+ # Let the system handle the storage. Write to syslog or STDERR.
10
+ # No log levels necessary. Just log whatever you want.
13
11
  #
14
12
  # Example:
15
13
  #
16
- # log(msg: "Oops !")
14
+ # log("Oops !", foo: {}, g: [])
17
15
  # #outputs:
18
16
  # # at=2013-03-07T09:21:39+00:00 pid=3242 app=some-process msg="Oops !" foo={} g=[]
19
17
  #
@@ -25,40 +23,57 @@ require 'forwardable'
25
23
  # ctx = Lines.context(encoding_id: Log.id)
26
24
  # ctx.log({})
27
25
  #
28
- # Lines.context(:foo => :bar) do |l|
29
- # l.log(:sadfasdf => 3)
26
+ # Lines.context(foo: 'bar') do |l|
27
+ # l.log(items_count: 3)
30
28
  # end
31
29
  module Lines
32
- # New lines in Lines
33
- NL = "\n".freeze
30
+ class << self
31
+ attr_reader :global
32
+ attr_writer :loader, :dumper
34
33
 
35
- @global = {}
36
- @outputters = []
34
+ # Parsing object. Responds to #load(string)
35
+ def loader
36
+ @loader ||= (
37
+ require 'lines/loader'
38
+ Loader
39
+ )
40
+ end
37
41
 
38
- class << self
42
+ # Serializing object. Responds to #dump(hash)
39
43
  def dumper; @dumper ||= Dumper.new end
40
- attr_reader :global
41
- attr_reader :outputters
42
44
 
43
- # Used to select what output the lines will be put on.
45
+ # Returns a backward-compatibile Logger
46
+ def logger
47
+ @logger ||= (
48
+ require 'lines/logger'
49
+ Logger.new(self)
50
+ )
51
+ end
52
+
53
+ # Used to configure lines.
44
54
  #
45
55
  # outputs - allows any kind of IO or Syslog
46
56
  #
47
57
  # Usage:
48
58
  #
49
- # Lines.use(Syslog, $stderr)
59
+ # Lines.use(Syslog, $stderr, at: proc{ Time.now })
50
60
  def use(*outputs)
51
- outputters.replace(outputs.flatten.map{|o| to_outputter o})
61
+ if outputs.last.kind_of?(Hash)
62
+ @global = outputs.pop
63
+ else
64
+ @global = {}
65
+ end
66
+ @outputters = outputs.flatten.map{|o| to_outputter o}
52
67
  end
53
68
 
54
69
  # The main function. Used to record objects in the logs as lines.
55
70
  #
56
- # obj - a ruby hash
57
- # args -
71
+ # obj - a ruby hash. coerced to +{"msg"=>obj}+ otherwise
72
+ # args - complementary values to put in the line
58
73
  def log(obj, args={})
59
74
  obj = prepare_obj(obj, args)
60
- outputters.each{|out| out.output(dumper, obj) }
61
- obj
75
+ @outputters.each{|out| out.output(dumper, obj) }
76
+ nil
62
77
  end
63
78
 
64
79
  # Add data to the logs
@@ -72,19 +87,21 @@ module Lines
72
87
  new_context
73
88
  end
74
89
 
75
- # Returns a backward-compatibile logger
76
- def logger
77
- @logger ||= (
78
- require 'lines/logger'
79
- Logger.new(self)
80
- )
81
- end
82
-
83
90
  def ensure_hash!(obj) # :nodoc:
84
91
  return {} unless obj
85
92
  return obj if obj.kind_of?(Hash)
86
93
  return obj.to_h if obj.respond_to?(:to_h)
87
- obj = {msg: obj}
94
+ {msg: obj}
95
+ end
96
+
97
+ # Parses a lines-formatted string
98
+ def load(string)
99
+ loader.load(string)
100
+ end
101
+
102
+ # Generates a lines-formatted string from the given object
103
+ def dump(obj)
104
+ dumper.dump ensure_hash!(obj)
88
105
  end
89
106
 
90
107
  protected
@@ -118,7 +135,7 @@ module Lines
118
135
  end
119
136
  end
120
137
 
121
- # Wrapper object that holds a given context. Emitted by Lines.with
138
+ # Wrapper object that holds a given context. Emitted by Lines.context
122
139
  class Context
123
140
  attr_reader :data
124
141
 
@@ -126,12 +143,16 @@ module Lines
126
143
  @data = data
127
144
  end
128
145
 
146
+ # Works like the Lines.log method.
129
147
  def log(obj, args={})
130
148
  Lines.log obj, Lines.ensure_hash!(args).merge(data)
131
149
  end
132
150
  end
133
151
 
152
+ # Handles output to any kind of IO
134
153
  class StreamOutputter
154
+ NL = "\n".freeze
155
+
135
156
  # stream must accept a #write(str) message
136
157
  def initialize(stream = $stderr)
137
158
  @stream = stream
@@ -141,28 +162,25 @@ module Lines
141
162
 
142
163
  def output(dumper, obj)
143
164
  str = dumper.dump(obj) + NL
144
- stream.write str
165
+ @stream.write str
145
166
  end
146
-
147
- protected
148
-
149
- attr_reader :stream
150
167
  end
151
168
 
152
169
  require 'syslog'
170
+ # Handles output to syslog
153
171
  class SyslogOutputter
154
172
  PRI2SYSLOG = {
155
- 'debug' => Syslog::LOG_DEBUG,
156
- 'info' => Syslog::LOG_INFO,
157
- 'warn' => Syslog::LOG_WARNING,
158
- 'warning' => Syslog::LOG_WARNING,
159
- 'err' => Syslog::LOG_ERR,
160
- 'error' => Syslog::LOG_ERR,
161
- 'crit' => Syslog::LOG_CRIT,
162
- 'critical' => Syslog::LOG_CRIT,
163
- }
164
-
165
- def initialize(syslog = Syslog)
173
+ 'debug' => ::Syslog::LOG_DEBUG,
174
+ 'info' => ::Syslog::LOG_INFO,
175
+ 'warn' => ::Syslog::LOG_WARNING,
176
+ 'warning' => ::Syslog::LOG_WARNING,
177
+ 'err' => ::Syslog::LOG_ERR,
178
+ 'error' => ::Syslog::LOG_ERR,
179
+ 'crit' => ::Syslog::LOG_CRIT,
180
+ 'critical' => ::Syslog::LOG_CRIT,
181
+ }.freeze
182
+
183
+ def initialize(syslog = ::Syslog)
166
184
  @syslog = syslog
167
185
  end
168
186
 
@@ -175,9 +193,8 @@ module Lines
175
193
  obj.delete(:app) # And again
176
194
 
177
195
  level = extract_pri(obj)
178
- str = dumper.dump(obj)
179
196
 
180
- @syslog.log(level, "%s", str)
197
+ @syslog.log(level, "%s", dumper.dump(obj))
181
198
  end
182
199
 
183
200
  protected
@@ -186,13 +203,13 @@ module Lines
186
203
  return if @syslog.opened?
187
204
  app_name ||= File.basename($0)
188
205
  @syslog.open(app_name,
189
- Syslog::LOG_PID | Syslog::LOG_CONS | Syslog::LOG_NDELAY,
190
- Syslog::LOG_USER)
206
+ ::Syslog::LOG_PID | ::Syslog::LOG_CONS | ::Syslog::LOG_NDELAY,
207
+ ::Syslog::LOG_USER)
191
208
  end
192
209
 
193
210
  def extract_pri(h)
194
211
  pri = h.delete(:pri).to_s.downcase
195
- PRI2SYSLOG[pri] || Syslog::LOG_INFO
212
+ PRI2SYSLOG[pri] || ::Syslog::LOG_INFO
196
213
  end
197
214
  end
198
215
 
@@ -230,15 +247,41 @@ module Lines
230
247
  # This dumper has been inspired by the OkJSON gem (both formats look alike
231
248
  # after all).
232
249
  class Dumper
250
+ SPACE = ' '
251
+ LIT_TRUE = '#t'
252
+ LIT_FALSE = '#f'
253
+ LIT_NIL = 'nil'
254
+ OPEN_BRACE = '{'
255
+ SHUT_BRACE = '}'
256
+ OPEN_BRACKET = '['
257
+ SHUT_BRACKET = ']'
258
+ SINGLE_QUOTE = "'"
259
+ DOUBLE_QUOTE = '"'
260
+
261
+ constants.each(&:freeze)
262
+
233
263
  def dump(obj) #=> String
234
264
  objenc_internal(obj)
235
265
  end
236
266
 
237
267
  # Used to introduce new ruby litterals.
268
+ #
269
+ # Usage:
270
+ #
271
+ # Point = Struct.new(:x, :y)
272
+ # Lines.dumper.map(Point) do |p|
273
+ # "#{p.x}x#{p.y}"
274
+ # end
275
+ #
276
+ # Lines.log msg: Point.new(3, 5)
277
+ # # logs: msg=3x5
278
+ #
238
279
  def map(klass, &rule)
239
280
  @mapping[klass] = rule
240
281
  end
241
282
 
283
+ # After a certain depth, arrays are replaced with [...] and objects with
284
+ # {...}. Default is 4.
242
285
  attr_accessor :max_depth
243
286
 
244
287
  protected
@@ -247,7 +290,7 @@ module Lines
247
290
 
248
291
  def initialize
249
292
  @mapping = {}
250
- @max_depth = 3
293
+ @max_depth = 4
251
294
  end
252
295
 
253
296
  def objenc_internal(x, depth=0)
@@ -255,7 +298,7 @@ module Lines
255
298
  if depth > max_depth
256
299
  '...'
257
300
  else
258
- x.map{|k,v| "#{keyenc(k)}=#{valenc(v, depth)}" }.join(' ')
301
+ x.map{|k,v| "#{keyenc(k)}=#{valenc(v, depth)}" }.join(SPACE)
259
302
  end
260
303
  end
261
304
 
@@ -273,39 +316,40 @@ module Lines
273
316
  when Array then arrenc(x, depth)
274
317
  when String, Symbol then strenc(x)
275
318
  when Numeric then numenc(x)
276
- when Time, Date then timeenc(x)
277
- when true then '#t'
278
- when false then '#f'
279
- when nil then 'nil'
319
+ when Time then timeenc(x)
320
+ when Date then dateenc(x)
321
+ when true then LIT_TRUE
322
+ when false then LIT_FALSE
323
+ when nil then LIT_NIL
280
324
  else
281
325
  litenc(x)
282
326
  end
283
327
  end
284
328
 
285
329
  def objenc(x, depth)
286
- '{' + objenc_internal(x, depth) + '}'
330
+ OPEN_BRACE + objenc_internal(x, depth) + SHUT_BRACE
287
331
  end
288
332
 
289
333
  def arrenc(a, depth)
290
334
  depth += 1
291
335
  # num + unit. Eg: 3ms
292
336
  if a.size == 2 && a.first.kind_of?(Numeric) && is_literal?(a.last.to_s)
293
- numenc(a.first) + strenc(a.last)
337
+ "#{numenc(a.first)}:#{strenc(a.last)}"
294
338
  elsif depth > max_depth
295
339
  '[...]'
296
340
  else
297
- '[' + a.map{|x| valenc(x, depth)}.join(' ') + ']'
341
+ OPEN_BRACKET + a.map{|x| valenc(x, depth)}.join(' ') + SHUT_BRACKET
298
342
  end
299
343
  end
300
344
 
301
- # TODO: Single-quote espace if possible
302
345
  def strenc(s)
303
346
  s = s.to_s
304
347
  unless is_literal?(s)
305
348
  s = s.inspect
306
- unless s[1..-2].include?("'")
307
- s[0] = s[-1] = "'"
308
- s.gsub!('\"', '"')
349
+ unless s[1..-2].include?(SINGLE_QUOTE)
350
+ s.gsub!(SINGLE_QUOTE, "\\'")
351
+ s.gsub!('\"', DOUBLE_QUOTE)
352
+ s[0] = s[-1] = SINGLE_QUOTE
309
353
  end
310
354
  end
311
355
  s
@@ -336,8 +380,12 @@ module Lines
336
380
  t.utc.iso8601
337
381
  end
338
382
 
383
+ def dateenc(d)
384
+ d.iso8601
385
+ end
386
+
339
387
  def is_literal?(s)
340
- !s.index(/[\s'"]/)
388
+ !s.index(/[\s'"=:{}\[\]]/)
341
389
  end
342
390
 
343
391
  end
@@ -360,3 +408,6 @@ module Lines
360
408
  end
361
409
  extend UniqueIDs
362
410
  end
411
+
412
+ # default config
413
+ Lines.use($stderr)
@@ -1,4 +1,5 @@
1
1
  require 'active_record'
2
+ require 'active_record/log_subscriber'
2
3
  require 'lines'
3
4
 
4
5
  module Lines
@@ -1,166 +1,229 @@
1
- begin
2
- require 'parslet'
3
- rescue LoadError
4
- warn "lines/loader depends on parslet"
5
- raise
6
- end
7
-
8
- # http://zerowidth.com/2013/02/24/parsing-toml-in-ruby-with-parslet.html
9
1
  module Lines
10
2
  module Error; end
11
- module ParseError; include Error; end
12
- module Loader; extend self
13
- def load(s)
14
- parser = Parser.new
15
- transformer = Transformer.new
16
-
17
- tree = parser.parse(s)
18
- #puts; p tree; puts
19
- transformer.apply(tree)
20
- rescue Parslet::ParseFailed => ex
21
- # Mark as being part of the Lines library
22
- ex.extend ParseError
23
- raise
3
+
4
+ class Loader
5
+ class ParseError < StandardError; include Error; end
6
+
7
+ DOT = '.'
8
+ EQUAL = '='
9
+ SPACE = ' '
10
+ OPEN_BRACKET = '['
11
+ SHUT_BRACKET = ']'
12
+ OPEN_BRACE = '{'
13
+ SHUT_BRACE = '}'
14
+ SINGLE_QUOTE = "'"
15
+ DOUBLE_QUOTE = '"'
16
+ BACKSLASH = '\\'
17
+
18
+ ESCAPED_SINGLE_QUOTE = "\\'"
19
+ ESCAPED_DOUBLE_QUOTE = '\"'
20
+
21
+ LITERAL_MATCH = /[^=\s}\]]+/
22
+ SINGLE_QUOTE_MATCH = /(?:\\.|[^'])*/
23
+ DOUBLE_QUOTE_MATCH = /(?:\\.|[^"])*/
24
+
25
+ NUM_MATCH = /-?(?:0|[1-9])\d*(?:\.\d+)?(?:[eE][+-]\d+)?/
26
+ ISO8601_ZULU_CAPTURE = /^(\d\d\d\d)-(\d\d)-(\d\d)T(\d\d):(\d\d):(\d\d)Z$/
27
+ NUM_CAPTURE = /^(#{NUM_MATCH})$/
28
+ UNIT_CAPTURE = /^(#{NUM_MATCH}):(.+)/
29
+
30
+ # Speeds parsing up a bit
31
+ constants.each(&:freeze)
32
+
33
+ EOF = nil
34
+
35
+
36
+ def self.load(string)
37
+ new.parse(string)
24
38
  end
25
- end
26
39
 
27
- # Mostly copied over from the JSON example:
28
- # https://github.com/kschiess/parslet/blob/master/example/json.rb
29
- #
30
- # TODO:
31
- # ISO8601 dates
32
- class Parser < Parslet::Parser
33
-
34
- rule(:spaces) { match(' ').repeat(1) }
35
- rule(:spaces?) { spaces.maybe }
36
-
37
- rule(:digit) { match['0-9'] }
38
-
39
- rule(:number) {
40
- (
41
- str('-').maybe >> (
42
- str('0') | (match['1-9'] >> digit.repeat)
43
- ) >> (
44
- str('.') >> digit.repeat(1)
45
- ).maybe >> (
46
- match('[eE]') >> (str('+') | str('-')).maybe >> digit.repeat(1)
47
- ).maybe
48
- ).as(:number)
49
- }
50
-
51
- rule(:time) {
52
- digit.repeat(4) >> str('-') >>
53
- digit.repeat(2) >> str('-') >>
54
- digit.repeat(2) >> str('T') >>
55
- digit.repeat(2) >> str(':') >>
56
- digit.repeat(2) >> str(':') >>
57
- digit.repeat(2) >> str('Z')
58
- }
59
-
60
- rule(:singlequoted_string) {
61
- str("'") >> (
62
- str('\\') >> any | str("'").absent? >> any
63
- ).repeat.as(:string) >> str("'")
64
- }
65
-
66
- rule(:doublequoted_string) {
67
- str('"') >> (
68
- str('\\') >> any | str('"').absent? >> any
69
- ).repeat.as(:string) >> str('"')
70
- }
71
-
72
- rule(:simple_string) {
73
- match['a-zA-Z_\-:'].repeat.as(:string)
74
- }
75
-
76
- rule(:string) {
77
- singlequoted_string | doublequoted_string | simple_string
78
- }
79
-
80
- rule(:array) {
81
- str('[') >> spaces? >>
82
- (value >> (spaces >> value).repeat).maybe.as(:array) >>
83
- spaces? >> str(']')
84
- }
85
-
86
- rule(:object) {
87
- str('{') >> spaces? >>
88
- (entry >> (spaces >> entry).repeat).maybe.as(:object) >>
89
- spaces? >> str('}')
90
- }
91
-
92
- rule(:key) {
93
- match['a-zA-Z0-9_'].repeat
94
- }
95
-
96
- rule(:value) {
97
- str('#t').as(:true) | str('#f').as(:false) |
98
- str('nil').as(:nil) |
99
- object | array |
100
- number | time |
101
- string
102
- }
103
-
104
- rule(:entry) {
105
- (
106
- key.as(:key) >>
107
- str('=') >>
108
- value.as(:val)
109
- ).as(:entry)
110
- }
111
-
112
- rule(:top) { spaces? >> (entry >> (spaces >> entry).repeat).maybe.as(:object) >> spaces? }
113
- #rule(:top) { (digit >> digit).as(:digit) }
114
- #rule(:top) { time }
115
-
116
- root(:top)
117
- end
40
+ def parse(string)
41
+ init(string.rstrip)
42
+ inner_obj
43
+ end
44
+
45
+ protected
46
+
47
+ def init(string)
48
+ @string = string
49
+ @pos = 0
50
+ @c = @string[0]
51
+ end
52
+
53
+ def getc
54
+ @pos += 1
55
+ @c = @string[@pos]
56
+ end
57
+
58
+ def accept(char)
59
+ if @c == char
60
+ getc
61
+ return true
62
+ end
63
+ false
64
+ end
65
+
66
+ def peek(num)
67
+ @string[@pos+num]
68
+ end
69
+
70
+ def skip(num)
71
+ @pos += num
72
+ @c = @string[@pos]
73
+ end
74
+
75
+ def match(reg)
76
+ @string.match(reg, @pos)
77
+ end
78
+
79
+ def expect(char)
80
+ if !accept(char)
81
+ fail "Expected '#{char}' but got '#{@c}'"
82
+ end
83
+ end
84
+
85
+ def fail(msg)
86
+ raise ParseError, "At #{@pos}, #{msg}"
87
+ end
88
+
89
+ def dbg(*x)
90
+ #p [@pos, @c, @string[0..@pos]] + x
91
+ end
92
+
93
+ # Structures
94
+
95
+
96
+ def inner_obj
97
+ dbg :inner_obj
98
+ # Shortcut for the '...' max_depth notation
99
+ if @c == DOT && peek(1) == DOT && peek(2) == DOT
100
+ expect DOT
101
+ expect DOT
102
+ expect DOT
103
+ return {'...' => ''}
104
+ end
105
+
106
+ return {} if @c == EOF || @c == SHUT_BRACE
118
107
 
119
- class Transformer < Parslet::Transform
108
+ # First pair
109
+ k = key()
110
+ expect EQUAL
111
+ obj = {
112
+ k => value()
113
+ }
114
+
115
+ while accept(SPACE)
116
+ k = key()
117
+ expect EQUAL
118
+ obj[k] = value()
119
+ end
120
+
121
+ obj
122
+ end
120
123
 
121
- class Entry < Struct.new(:key, :val); end
124
+ def key
125
+ dbg :key
122
126
 
123
- rule(array: subtree(:ar)) {
124
- case ar
125
- when nil
126
- []
127
- when Array
128
- ar
127
+ if @c == SINGLE_QUOTE
128
+ single_quoted_string
129
+ elsif @c == DOUBLE_QUOTE
130
+ double_quoted_string
129
131
  else
130
- [ar]
132
+ literal(false)
131
133
  end
132
- }
133
- rule(object: subtree(:ob)) {
134
- case ob
135
- when nil
136
- []
137
- when Array
138
- ob
134
+ end
135
+
136
+ def single_quoted_string
137
+ dbg :single_quoted_string
138
+
139
+ expect SINGLE_QUOTE
140
+ md = match SINGLE_QUOTE_MATCH
141
+ str = md[0].gsub ESCAPED_SINGLE_QUOTE, SINGLE_QUOTE
142
+ skip md[0].size
143
+
144
+ expect SINGLE_QUOTE
145
+ str
146
+ end
147
+
148
+ def double_quoted_string
149
+ dbg :double_quoted_string
150
+
151
+ expect DOUBLE_QUOTE
152
+ md = match DOUBLE_QUOTE_MATCH
153
+ str = md[0].gsub ESCAPED_DOUBLE_QUOTE, DOUBLE_QUOTE
154
+ skip md[0].size
155
+
156
+ expect DOUBLE_QUOTE
157
+ str
158
+ end
159
+
160
+ def literal(sub_parse)
161
+ dbg :literal, sub_parse
162
+
163
+ return "" unless ((md = match LITERAL_MATCH))
164
+
165
+ literal = md[0]
166
+ skip literal.size
167
+
168
+ return literal unless sub_parse
169
+
170
+ case literal
171
+ when 'nil'
172
+ nil
173
+ when '#t'
174
+ true
175
+ when '#f'
176
+ false
177
+ when ISO8601_ZULU_CAPTURE
178
+ Time.new($1.to_i, $2.to_i, $3.to_i, $4.to_i, $5.to_i, $6.to_i, '+00:00').utc
179
+ when NUM_CAPTURE
180
+ literal.index('.') ? Float(literal) : Integer(literal)
181
+ when UNIT_CAPTURE
182
+ num = $1.index('.') ? Float($1) : Integer($1)
183
+ unit = $2
184
+ [num, unit]
139
185
  else
140
- [ob]
141
- end.inject({}) { |h, e|
142
- h[e[:entry][:key].to_s] = e[:entry][:val]; h
143
- }
144
- }
186
+ literal
187
+ end
188
+ end
145
189
 
146
- # rule(entry: { key: simple(:ke), val: simple(:va) }) {
147
- # Entry.new(ke.to_s, va)
148
- # }
190
+ def value
191
+ dbg :value
192
+
193
+ case @c
194
+ when OPEN_BRACKET
195
+ list
196
+ when OPEN_BRACE
197
+ object
198
+ when DOUBLE_QUOTE
199
+ double_quoted_string
200
+ when SINGLE_QUOTE
201
+ single_quoted_string
202
+ else
203
+ literal(:sub_parse)
204
+ end
205
+ end
149
206
 
150
- rule(time: { year: simple(:ye), month: simple(:mo), day: simple(:da), hour: simple(:ho), minute: simple(:min), second: simple(:sec)}) {
151
- Time.new(ye.to_i, mo.to_i, da.to_i, ho.to_i, min.to_i, sec.to_i, "+00:00")
152
- }
207
+ def list
208
+ dbg :list
153
209
 
154
- rule(string: simple(:st)) {
155
- st.to_s
156
- }
210
+ list = []
211
+ expect(OPEN_BRACKET)
212
+ list.push value
213
+ while accept(SPACE)
214
+ list.push value
215
+ end
216
+ expect(SHUT_BRACKET)
217
+ list
218
+ end
157
219
 
158
- rule(number: simple(:nb)) {
159
- nb.match(/[eE\.]/) ? Float(nb) : Integer(nb)
160
- }
220
+ def object
221
+ dbg :object
161
222
 
162
- rule(nil: simple(:ni)) { nil }
163
- rule(true: simple(:tr)) { true }
164
- rule(false: simple(:fa)) { false }
223
+ expect(OPEN_BRACE)
224
+ obj = inner_obj
225
+ expect(SHUT_BRACE)
226
+ obj
227
+ end
165
228
  end
166
229
  end
@@ -1,3 +1,3 @@
1
1
  module Lines
2
- VERSION = "0.1.27"
2
+ VERSION = "0.2.0"
3
3
  end
@@ -1,56 +1,46 @@
1
+ require 'benchmark/ips'
2
+
1
3
  $:.unshift File.expand_path('../../lib', __FILE__)
2
4
  require 'lines'
3
5
 
4
- module Kernel
5
- def bm(name = nil, &what)
6
- start = Time.now.to_f
7
- count = 0
8
- max = 0.5
9
- name ||= what.source_location.join(':')
10
- $stdout.write "#{name} : "
11
- while Time.now.to_f - start < max
12
- yield
13
- count += 1
14
- end
15
- $stdout.puts "%0.3f fps" % (count / max)
16
- end
17
- end
18
-
19
6
  class FakeIO
20
7
  def write(*a)
21
8
  end
22
9
  alias syswrite write
23
10
  end
24
11
 
25
- Lines.global[:app] = 'benchmark'
26
- Lines.global[:at] = proc{ Time.now }
27
- Lines.global[:pid] = Process.pid
12
+ globals = {
13
+ app: 'benchmark',
14
+ at: proc{ Time.now },
15
+ pid: Process.pid,
16
+ }
28
17
 
29
18
  EX = (raise "FOO" rescue $!)
30
19
 
31
- Lines.use FakeIO.new
32
- bm "FakeIO write" do
33
- Lines.log EX
34
- end
20
+ Benchmark.ips do |x|
21
+ x.report "FakeIO write" do |n|
22
+ Lines.use(FakeIO.new, globals)
23
+ n.times{ Lines.log EX }
24
+ end
35
25
 
36
- dev_null = File.open('/dev/null', 'w')
37
- Lines.use dev_null
38
- bm "/dev/null write" do
39
- Lines.log EX
40
- end
26
+ x.report "/dev/null write" do |n|
27
+ dev_null = File.open('/dev/null', 'w')
28
+ Lines.use(dev_null, globals)
29
+ n.times{ Lines.log EX }
30
+ end
41
31
 
42
- Lines.use Syslog
43
- bm "syslog write" do
44
- Lines.log EX
45
- end
32
+ x.report "syslog write" do |n|
33
+ Lines.use(Syslog, globals)
34
+ n.times{ Lines.log EX }
35
+ end
46
36
 
47
- real_file = File.open('real_file.log', 'w')
48
- Lines.use real_file
49
- bm "real file" do
50
- Lines.log EX
51
- end
37
+ x.report "real file" do |n|
38
+ real_file = File.open('real_file.log', 'w')
39
+ Lines.use(real_file, globals)
40
+ n.times{ Lines.log EX }
41
+ end
52
42
 
53
- bm "real file logger" do
54
- Lines.logger.info "Ahoi this is a really cool option"
43
+ x.report "real file logger" do |n|
44
+ n.times{ Lines.logger.info "Ahoi this is a really cool option" }
45
+ end
55
46
  end
56
-
@@ -1,61 +1,55 @@
1
1
  require 'spec_helper'
2
2
  require 'lines/loader'
3
3
 
4
- module Lines
5
- describe Loader do
6
- subject { Loader }
7
-
8
- it "can load stuff" do
9
- expect(Loader.load 'foo=bar').to eq("foo" => "bar")
10
- end
11
- end
12
-
13
- describe Parser do
14
- let(:parser) { Lines::Parser.new }
15
-
16
- context "value parsing" do
17
- let(:value_parser) { parser.value }
18
-
19
- it "parses integers" do
20
- pending
21
- expect(value_parser).to parse("1")
22
- expect(value_parser).to parse("-123")
23
- expect(value_parser).to parse("120381")
24
- expect(value_parser).to parse("181")
25
- end
26
-
27
- it "parses floats" do
28
- pending
29
- expect(value_parser).to parse("0.1")
30
- expect(value_parser).to parse("3.14159")
31
- expect(value_parser).to parse("-0.00001")
32
- end
33
-
34
- it "parses booleans" do
35
- pending
36
- expect(value_parser).to parse("#t")
37
- expect(value_parser).to parse("#f")
38
- end
39
-
40
- it "parses datetimes" do
41
- pending
42
- expect(value_parser).to parse("1979-05-27T07:32:00Z")
43
- expect(value_parser).to parse("2013-02-24T17:26:21Z")
44
- expect(value_parser).to_not parse("1979l05-27 07:32:00")
45
- end
46
-
47
- it "parses strings" do
48
- pending
49
- expect(value_parser).to parse('""')
50
- expect(value_parser).to parse('"hello world"')
51
- expect(value_parser).to parse('"hello\\nworld"')
52
- expect(value_parser).to parse('"hello\\t\\n\\\\\\0world\\n"')
53
- expect(value_parser).to_not parse("\"hello\nworld\"")
54
- end
55
- end
56
- end
57
-
58
- describe Transformer do
4
+ describe Lines::Loader do
5
+ subject { Lines::Loader.new }
59
6
 
7
+ def expect_load(str)
8
+ expect(subject.parse str)
9
+ end
10
+
11
+ it "can load stuff" do
12
+ expect_load('foo=bar bar=33').to eq("foo" => "bar", "bar" => 33)
13
+ end
14
+
15
+ it "handles max_depth items" do
16
+ expect_load('x=[...]').to eq("x" => ["..."])
17
+ expect_load('x={...}').to eq("x" => {"..." => ""})
18
+ end
19
+
20
+ it "treats missing value in a pair as an empty string" do
21
+ expect_load('x=').to eq("x" => "")
22
+ end
23
+
24
+ it "has non-greedy string parsing" do
25
+ expect_load('x="foo" bar="baz"').to eq("x" => "foo", "bar" => "baz")
26
+ end
27
+
28
+ it "unscapes quotes in quoted strings" do
29
+ expect_load("x='foo\\'bar'").to eq("x" => "foo'bar")
30
+ expect_load('x="foo\"bar"').to eq("x" => 'foo"bar')
31
+ end
32
+
33
+ it "doesn't parse literals when they are keys" do
34
+ expect_load("3=4").to eq("3" => 4)
35
+ end
36
+
37
+ it "handles some random stuff" do
38
+ expect_load("=").to eq("" => "")
39
+ expect_load('"\""=zzz').to eq('"' => "zzz")
40
+ end
41
+
42
+ it "parses sample log lines" do
43
+ expect_load("commit=716f337").to eq("commit" => "716f337")
44
+
45
+ line = <<LINE
46
+ at=2013-07-12T21:33:47Z commit=716f337 sql="SELECT FROM_UNIXTIME(UNIX_TIMESTAMP(created_at) - UNIX_TIMESTAMP(created_at)%(300)) as timestamp FROM `job_queue_logs` WHERE `job_queue_logs`.`account_id` = 'effe376baf553c590c02090abe512278' AND (created_at >= '2013-06-28 16:56:12') GROUP BY timestamp" elapsed=31.9:ms
47
+ LINE
48
+ expect_load(line).to eq(
49
+ "at" => Time.at(1373664827).utc,
50
+ "commit" => "716f337",
51
+ "sql" => "SELECT FROM_UNIXTIME(UNIX_TIMESTAMP(created_at) - UNIX_TIMESTAMP(created_at)%(300)) as timestamp FROM `job_queue_logs` WHERE `job_queue_logs`.`account_id` = 'effe376baf553c590c02090abe512278' AND (created_at >= '2013-06-28 16:56:12') GROUP BY timestamp",
52
+ "elapsed" => [31.9, "ms"],
53
+ )
60
54
  end
61
55
  end
@@ -2,28 +2,29 @@ require 'spec_helper'
2
2
  require 'lines'
3
3
  require 'stringio'
4
4
 
5
+ NL = "\n"
6
+
5
7
  describe Lines do
6
8
  let(:outputter) { StringIO.new }
7
9
  let(:output) { outputter.string }
8
10
  before do
9
- Lines.global.replace({})
10
11
  Lines.use(outputter)
11
12
  end
12
13
 
13
14
  context ".log" do
14
15
  it "logs stuff" do
15
16
  Lines.log(foo: 'bar')
16
- expect(output).to eq('foo=bar' + Lines::NL)
17
+ expect(output).to eq('foo=bar' + NL)
17
18
  end
18
19
 
19
20
  it "supports a first msg argument" do
20
21
  Lines.log("this user is annoying", user: 'bob')
21
- expect(output).to eq("msg='this user is annoying' user=bob" + Lines::NL)
22
+ expect(output).to eq("msg='this user is annoying' user=bob" + NL)
22
23
  end
23
24
 
24
25
  it "logs exceptions" do
25
26
  Lines.log(StandardError.new("error time!"), user: 'bob')
26
- expect(output).to eq("ex=StandardError msg='error time!' user=bob" + Lines::NL)
27
+ expect(output).to eq("ex=StandardError msg='error time!' user=bob" + NL)
27
28
  end
28
29
 
29
30
  it "logs exception backtraces when available" do
@@ -35,33 +36,33 @@ describe Lines do
35
36
 
36
37
  it "works with anything" do
37
38
  Lines.log("anything1", "anything2")
38
- expect(output).to eq('msg=anything2' + Lines::NL)
39
+ expect(output).to eq('msg=anything2' + NL)
39
40
  end
40
41
 
41
42
  it "doesn't convert nil args to msg" do
42
43
  Lines.log("anything", nil)
43
- expect(output).to eq('msg=anything' + Lines::NL)
44
+ expect(output).to eq('msg=anything' + NL)
44
45
  end
45
46
  end
46
47
 
47
48
  context ".context" do
48
49
  it "has contextes" do
49
50
  Lines.context(foo: "bar").log(a: 'b')
50
- expect(output).to eq('a=b foo=bar' + Lines::NL)
51
+ expect(output).to eq('a=b foo=bar' + NL)
51
52
  end
52
53
 
53
54
  it "has contextes with blocks" do
54
55
  Lines.context(foo: "bar") do |ctx|
55
56
  ctx.log(a: 'b')
56
57
  end
57
- expect(output).to eq('a=b foo=bar' + Lines::NL)
58
+ expect(output).to eq('a=b foo=bar' + NL)
58
59
  end
59
60
 
60
61
  it "mixes everything" do
61
62
  Lines.global[:app] = :self
62
63
  ctx = Lines.context(foo: "bar")
63
64
  ctx.log('msg', ahoi: true)
64
- expect(output).to eq('app=self msg=msg ahoi=#t foo=bar' + Lines::NL)
65
+ expect(output).to eq('app=self msg=msg ahoi=#t foo=bar' + NL)
65
66
  end
66
67
  end
67
68
 
@@ -69,7 +70,7 @@ describe Lines do
69
70
  it "is provided for backward-compatibility" do
70
71
  l = Lines.logger
71
72
  l.info("hi")
72
- expect(output).to eq('pri=info msg=hi' + Lines::NL)
73
+ expect(output).to eq('pri=info msg=hi' + NL)
73
74
  end
74
75
  end
75
76
 
@@ -77,7 +78,7 @@ describe Lines do
77
78
  it "prepends data to the line" do
78
79
  Lines.global["app"] = :self
79
80
  Lines.log 'hey'
80
- expect(output).to eq('app=self msg=hey' + Lines::NL)
81
+ expect(output).to eq('app=self msg=hey' + NL)
81
82
  end
82
83
 
83
84
  it "resolves procs dynamically" do
@@ -86,15 +87,15 @@ describe Lines do
86
87
  Lines.log 'test1'
87
88
  Lines.log 'test2'
88
89
  expect(output).to eq(
89
- 'count=1 msg=test1' + Lines::NL +
90
- 'count=2 msg=test2' + Lines::NL
90
+ 'count=1 msg=test1' + NL +
91
+ 'count=2 msg=test2' + NL
91
92
  )
92
93
  end
93
94
 
94
95
  it "doesn't fail if a proc has an exception" do
95
96
  Lines.global[:X] = proc{ fail "error" }
96
97
  Lines.log 'test'
97
- expect(output).to eq("X='#<RuntimeError: error>' msg=test" + Lines::NL)
98
+ expect(output).to eq("X='#<RuntimeError: error>' msg=test" + NL)
98
99
  end
99
100
  end
100
101
  end
@@ -128,18 +129,22 @@ describe Lines::Dumper do
128
129
  end
129
130
 
130
131
  it "can dump a basicobject" do
131
- expect_dump(foo: BasicObject.new).to match(/foo=#<BasicObject:0x[0-9a-f]+>/)
132
+ expect_dump(foo: BasicObject.new).to match(/foo='#<BasicObject:0x[0-9a-f]+>'/)
132
133
  end
133
134
 
134
135
  it "can dump IO objects" do
135
136
  expect_dump(foo: File.open(__FILE__)).to match(/foo='?#<File:[^>]+>'?/)
136
- expect_dump(foo: STDOUT).to match(/^foo=(?:#<IO:<STDOUT>>|'#<IO:fd 1>')$/)
137
+ expect_dump(foo: STDOUT).to match(/^foo='(?:#<IO:<STDOUT>>|#<IO:fd 1>)'$/)
137
138
  end
138
139
 
139
140
  it "dumps time as ISO zulu format" do
140
141
  expect_dump(foo: Time.at(1337)).to eq('foo=1970-01-01T00:22:17Z')
141
142
  end
142
143
 
144
+ it "dumps date as ISO date" do
145
+ expect_dump(foo: Date.new(1968, 3, 7)).to eq('foo=1968-03-07')
146
+ end
147
+
143
148
  it "dumps symbols as strings" do
144
149
  expect_dump(foo: :some_symbol).to eq('foo=some_symbol')
145
150
  expect_dump(foo: :"some symbol").to eq("foo='some symbol'")
@@ -158,14 +163,14 @@ describe Lines::Dumper do
158
163
  end
159
164
 
160
165
  it "dumps [number, literal] tuples as numberliteral" do
161
- expect_dump(foo: [3, :ms]).to eq('foo=3ms')
162
- expect_dump(foo: [54.2, 's']).to eq('foo=54.2s')
166
+ expect_dump(foo: [3, :ms]).to eq('foo=3:ms')
167
+ expect_dump(foo: [54.2, 's']).to eq('foo=54.2:s')
163
168
  end
164
169
 
165
170
  it "knows how to handle circular dependencies" do
166
171
  x = {}
167
172
  x[:x] = x
168
- expect_dump(x).to eq('x={x={x={...}}}')
173
+ expect_dump(x).to eq('x={x={x={x={...}}}}')
169
174
  end
170
175
  end
171
176
 
@@ -0,0 +1,26 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ libdir = File.expand_path('../../lib', __FILE__)
4
+ $:.unshift(libdir) unless $:.include? libdir
5
+
6
+ require 'lines'
7
+
8
+ loader = Lines.loader
9
+
10
+ start = Time.now
11
+ line_count = 0
12
+
13
+ $stdin.lines.each do |line|
14
+ begin
15
+ line_count += 1
16
+ loader.load(line)
17
+ rescue Lines::Error => ex
18
+ # Lines seem to get truncated by syslog when too long
19
+ if line.size < 2000
20
+ p line
21
+ p ex
22
+ end
23
+ end
24
+ end
25
+
26
+ puts "Parsed #{line_count / (Time.now - start)} lines per second"
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: lines
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.1.27
4
+ version: 0.2.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Jonas Pfenniger
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2013-07-10 00:00:00.000000000 Z
11
+ date: 2013-07-15 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: bundler
@@ -63,6 +63,7 @@ files:
63
63
  - spec/bench.rb
64
64
  - spec/lines_loader_spec.rb
65
65
  - spec/lines_spec.rb
66
+ - spec/parse-bench
66
67
  - spec/spec_helper.rb
67
68
  homepage: https://github.com/zimbatm/lines-ruby
68
69
  licenses:
@@ -92,4 +93,5 @@ test_files:
92
93
  - spec/bench.rb
93
94
  - spec/lines_loader_spec.rb
94
95
  - spec/lines_spec.rb
96
+ - spec/parse-bench
95
97
  - spec/spec_helper.rb