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)