lines 0.1.27 → 0.2.0

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