lib/lines.rb in lines-0.1.27 vs lib/lines.rb in lines-0.2.0
- old
+ new
@@ -1,21 +1,19 @@
require 'date'
require 'time'
-require 'forwardable'
-# Lines is an opinionated structured log format and a library
-# inspired by Slogger.
+# Lines is an opinionated structured log format and a library.
#
-# Don't use log levels. They limit the reasoning of the developer.
# Log everything in development AND production.
# Logs should be easy to read, grep and parse.
# Logging something should never fail.
-# Use syslog.
+# Let the system handle the storage. Write to syslog or STDERR.
+# No log levels necessary. Just log whatever you want.
#
# Example:
#
-# log(msg: "Oops !")
+# log("Oops !", foo: {}, g: [])
# #outputs:
# # at=2013-03-07T09:21:39+00:00 pid=3242 app=some-process msg="Oops !" foo={} g=[]
#
# Usage:
#
@@ -23,44 +21,61 @@
# Lines.log(foo: 3, msg: "This")
#
# ctx = Lines.context(encoding_id: Log.id)
# ctx.log({})
#
-# Lines.context(:foo => :bar) do |l|
-# l.log(:sadfasdf => 3)
+# Lines.context(foo: 'bar') do |l|
+# l.log(items_count: 3)
# end
module Lines
- # New lines in Lines
- NL = "\n".freeze
+ class << self
+ attr_reader :global
+ attr_writer :loader, :dumper
- @global = {}
- @outputters = []
+ # Parsing object. Responds to #load(string)
+ def loader
+ @loader ||= (
+ require 'lines/loader'
+ Loader
+ )
+ end
- class << self
+ # Serializing object. Responds to #dump(hash)
def dumper; @dumper ||= Dumper.new end
- attr_reader :global
- attr_reader :outputters
- # Used to select what output the lines will be put on.
+ # Returns a backward-compatibile Logger
+ def logger
+ @logger ||= (
+ require 'lines/logger'
+ Logger.new(self)
+ )
+ end
+
+ # Used to configure lines.
#
# outputs - allows any kind of IO or Syslog
#
# Usage:
#
- # Lines.use(Syslog, $stderr)
+ # Lines.use(Syslog, $stderr, at: proc{ Time.now })
def use(*outputs)
- outputters.replace(outputs.flatten.map{|o| to_outputter o})
+ if outputs.last.kind_of?(Hash)
+ @global = outputs.pop
+ else
+ @global = {}
+ end
+ @outputters = outputs.flatten.map{|o| to_outputter o}
end
# The main function. Used to record objects in the logs as lines.
#
- # obj - a ruby hash
- # args -
+ # obj - a ruby hash. coerced to +{"msg"=>obj}+ otherwise
+ # args - complementary values to put in the line
def log(obj, args={})
obj = prepare_obj(obj, args)
- outputters.each{|out| out.output(dumper, obj) }
- obj
+ @outputters.each{|out| out.output(dumper, obj) }
+ nil
end
# Add data to the logs
#
# data - a ruby hash
@@ -70,25 +85,27 @@
new_context = Context.new ensure_hash!(data)
yield new_context if block_given?
new_context
end
- # Returns a backward-compatibile logger
- def logger
- @logger ||= (
- require 'lines/logger'
- Logger.new(self)
- )
- end
-
def ensure_hash!(obj) # :nodoc:
return {} unless obj
return obj if obj.kind_of?(Hash)
return obj.to_h if obj.respond_to?(:to_h)
- obj = {msg: obj}
+ {msg: obj}
end
+ # Parses a lines-formatted string
+ def load(string)
+ loader.load(string)
+ end
+
+ # Generates a lines-formatted string from the given object
+ def dump(obj)
+ dumper.dump ensure_hash!(obj)
+ end
+
protected
def prepare_obj(obj, args={})
if obj.kind_of?(Exception)
ex = obj
@@ -116,55 +133,56 @@
return SyslogOutputter.new if out == ::Syslog
raise ArgumentError, "unknown outputter #{out.inspect}"
end
end
- # Wrapper object that holds a given context. Emitted by Lines.with
+ # Wrapper object that holds a given context. Emitted by Lines.context
class Context
attr_reader :data
def initialize(data)
@data = data
end
+ # Works like the Lines.log method.
def log(obj, args={})
Lines.log obj, Lines.ensure_hash!(args).merge(data)
end
end
+ # Handles output to any kind of IO
class StreamOutputter
+ NL = "\n".freeze
+
# stream must accept a #write(str) message
def initialize(stream = $stderr)
@stream = stream
# Is this needed ?
@stream.sync = true if @stream.respond_to?(:sync)
end
def output(dumper, obj)
str = dumper.dump(obj) + NL
- stream.write str
+ @stream.write str
end
-
- protected
-
- attr_reader :stream
end
require 'syslog'
+ # Handles output to syslog
class SyslogOutputter
PRI2SYSLOG = {
- 'debug' => Syslog::LOG_DEBUG,
- 'info' => Syslog::LOG_INFO,
- 'warn' => Syslog::LOG_WARNING,
- 'warning' => Syslog::LOG_WARNING,
- 'err' => Syslog::LOG_ERR,
- 'error' => Syslog::LOG_ERR,
- 'crit' => Syslog::LOG_CRIT,
- 'critical' => Syslog::LOG_CRIT,
- }
+ 'debug' => ::Syslog::LOG_DEBUG,
+ 'info' => ::Syslog::LOG_INFO,
+ 'warn' => ::Syslog::LOG_WARNING,
+ 'warning' => ::Syslog::LOG_WARNING,
+ 'err' => ::Syslog::LOG_ERR,
+ 'error' => ::Syslog::LOG_ERR,
+ 'crit' => ::Syslog::LOG_CRIT,
+ 'critical' => ::Syslog::LOG_CRIT,
+ }.freeze
- def initialize(syslog = Syslog)
+ def initialize(syslog = ::Syslog)
@syslog = syslog
end
def output(dumper, obj)
prepare_syslog obj[:app]
@@ -173,28 +191,27 @@
obj.delete(:pid) # It's going to be part of the message
obj.delete(:at) # Also part of the message
obj.delete(:app) # And again
level = extract_pri(obj)
- str = dumper.dump(obj)
- @syslog.log(level, "%s", str)
+ @syslog.log(level, "%s", dumper.dump(obj))
end
protected
def prepare_syslog(app_name)
return if @syslog.opened?
app_name ||= File.basename($0)
@syslog.open(app_name,
- Syslog::LOG_PID | Syslog::LOG_CONS | Syslog::LOG_NDELAY,
- Syslog::LOG_USER)
+ ::Syslog::LOG_PID | ::Syslog::LOG_CONS | ::Syslog::LOG_NDELAY,
+ ::Syslog::LOG_USER)
end
def extract_pri(h)
pri = h.delete(:pri).to_s.downcase
- PRI2SYSLOG[pri] || Syslog::LOG_INFO
+ PRI2SYSLOG[pri] || ::Syslog::LOG_INFO
end
end
# Some opinions here as well on the format:
#
@@ -228,36 +245,62 @@
# The output ought to use the UTF-8 encoding.
#
# This dumper has been inspired by the OkJSON gem (both formats look alike
# after all).
class Dumper
+ SPACE = ' '
+ LIT_TRUE = '#t'
+ LIT_FALSE = '#f'
+ LIT_NIL = 'nil'
+ OPEN_BRACE = '{'
+ SHUT_BRACE = '}'
+ OPEN_BRACKET = '['
+ SHUT_BRACKET = ']'
+ SINGLE_QUOTE = "'"
+ DOUBLE_QUOTE = '"'
+
+ constants.each(&:freeze)
+
def dump(obj) #=> String
objenc_internal(obj)
end
# Used to introduce new ruby litterals.
+ #
+ # Usage:
+ #
+ # Point = Struct.new(:x, :y)
+ # Lines.dumper.map(Point) do |p|
+ # "#{p.x}x#{p.y}"
+ # end
+ #
+ # Lines.log msg: Point.new(3, 5)
+ # # logs: msg=3x5
+ #
def map(klass, &rule)
@mapping[klass] = rule
end
+ # After a certain depth, arrays are replaced with [...] and objects with
+ # {...}. Default is 4.
attr_accessor :max_depth
protected
attr_reader :mapping
def initialize
@mapping = {}
- @max_depth = 3
+ @max_depth = 4
end
def objenc_internal(x, depth=0)
depth += 1
if depth > max_depth
'...'
else
- x.map{|k,v| "#{keyenc(k)}=#{valenc(v, depth)}" }.join(' ')
+ x.map{|k,v| "#{keyenc(k)}=#{valenc(v, depth)}" }.join(SPACE)
end
end
def keyenc(k)
case k
@@ -271,43 +314,44 @@
case x
when Hash then objenc(x, depth)
when Array then arrenc(x, depth)
when String, Symbol then strenc(x)
when Numeric then numenc(x)
- when Time, Date then timeenc(x)
- when true then '#t'
- when false then '#f'
- when nil then 'nil'
+ when Time then timeenc(x)
+ when Date then dateenc(x)
+ when true then LIT_TRUE
+ when false then LIT_FALSE
+ when nil then LIT_NIL
else
litenc(x)
end
end
def objenc(x, depth)
- '{' + objenc_internal(x, depth) + '}'
+ OPEN_BRACE + objenc_internal(x, depth) + SHUT_BRACE
end
def arrenc(a, depth)
depth += 1
# num + unit. Eg: 3ms
if a.size == 2 && a.first.kind_of?(Numeric) && is_literal?(a.last.to_s)
- numenc(a.first) + strenc(a.last)
+ "#{numenc(a.first)}:#{strenc(a.last)}"
elsif depth > max_depth
'[...]'
else
- '[' + a.map{|x| valenc(x, depth)}.join(' ') + ']'
+ OPEN_BRACKET + a.map{|x| valenc(x, depth)}.join(' ') + SHUT_BRACKET
end
end
- # TODO: Single-quote espace if possible
def strenc(s)
s = s.to_s
unless is_literal?(s)
s = s.inspect
- unless s[1..-2].include?("'")
- s[0] = s[-1] = "'"
- s.gsub!('\"', '"')
+ unless s[1..-2].include?(SINGLE_QUOTE)
+ s.gsub!(SINGLE_QUOTE, "\\'")
+ s.gsub!('\"', DOUBLE_QUOTE)
+ s[0] = s[-1] = SINGLE_QUOTE
end
end
s
end
@@ -334,12 +378,16 @@
def timeenc(t)
t.utc.iso8601
end
+ def dateenc(d)
+ d.iso8601
+ end
+
def is_literal?(s)
- !s.index(/[\s'"]/)
+ !s.index(/[\s'"=:{}\[\]]/)
end
end
require 'securerandom'
@@ -358,5 +406,8 @@
SecureRandom.urlsafe_base64(num_bytes)
end
end
extend UniqueIDs
end
+
+# default config
+Lines.use($stderr)