class StructuredLog

Constants

DEFAULT_DIR_PATH
DEFAULT_FILE_NAME
DEFAULT_XML_INDENTATION
DEFAULT_XML_ROOT_TAG_NAME
NO_BLOCK_GIVEN_MSG

Message for no block error.

NO_NEW_MSG

Message for calling-new error.

VERSION

Attributes

backtrace_filter[RW]
file[RW]
file_path[RW]
root_name[RW]
xml_indentation[RW]

Public Class Methods

new(file_path = File.join(DEFAULT_DIR_PATH, DEFAULT_FILE_NAME), options = Hash.new, im_ok_youre_not_ok = false) click to toggle source
# File lib/structured_log.rb, line 189
def initialize(file_path = File.join(DEFAULT_DIR_PATH, DEFAULT_FILE_NAME), options = Hash.new, im_ok_youre_not_ok = false)
  unless im_ok_youre_not_ok
    # Caller should call StructuredLog.open, not StructuredLog.new.
    raise RuntimeError.new(NO_NEW_MSG)
  end
  self.file_path = file_path
  self.root_name = options[:root_name]
  self.xml_indentation = options[:xml_indentation]
  self.backtrace_filter = options[:backtrace_filter] || /structured_log|ruby/
  self.file = File.open(self.file_path, 'w')
  log_puts("REMARK\tThis text log is the precursor for an XML log.")
  log_puts("REMARK\tIf the logged process completes, this text will be converted to XML.")
  log_puts("BEGIN\t#{self.root_name}")
  nil
end
open(file_path = File.join(DEFAULT_DIR_PATH, DEFAULT_FILE_NAME), options=Hash.new) { |log| ... } click to toggle source

Callers should call this method, not method new. file_path is the path to the output log file. Options can include:

  • :root_name => root-xml-tag-name.

  • :xml_indentation => Integer: indentation for nesting XML sub-elements.

# File lib/structured_log.rb, line 29
def self.open(file_path = File.join(DEFAULT_DIR_PATH, DEFAULT_FILE_NAME), options=Hash.new)
  raise NO_BLOCK_GIVEN_MSG unless (block_given?)
  default_options = Hash[
      :root_name => DEFAULT_XML_ROOT_TAG_NAME,
      :xml_indentation => DEFAULT_XML_INDENTATION
  ]
  options = default_options.merge(options)
  log = self.new(file_path, options, im_ok_youre_not_ok = true)
  begin
    yield log
  rescue => x
    log.put_element('uncaught_exception', :timestamp, :class => x.class) do
      log.put_element('message', x.message)
      log.put_element('backtrace') do
        backtrace = log.send(:filter_backtrace, x.backtrace)
        log.send(:put_cdata, backtrace)
      end
    end
  end
  log.send(:dispose)
  log.file_path
end

Private Class Methods

timestamp() click to toggle source

Return a timestamp string. The important property of this string is that it can be incorporated into a legal directory path (i.e., has no colons, etc.).

# File lib/structured_log.rb, line 311
def self.timestamp
  now = Time.now
  ts = now.strftime('%Y-%m-%d-%a-%H.%M.%S')
  usec_s = (now.usec / 1000).to_s
  while usec_s.length < 3 do
    usec_s = '0' + usec_s
  end
  # noinspection RubyUnusedLocalVariable
  ts += ".#{usec_s}"
end

Public Instance Methods

comment(text, *args) click to toggle source
# File lib/structured_log.rb, line 63
def comment(text, *args)
  put_element('comment', text, *args)
  nil
end
put_array(name, obj)
Alias for: put_each_with_index
put_cdata(text) click to toggle source
# File lib/structured_log.rb, line 174
def put_cdata(text)
  # Guard against using a terminator that's a substring of the cdata.
  s = 'EOT'
  terminator = s
  while text.match(terminator) do
    terminator += s
  end
  log_puts("CDATA\t<<#{terminator}")
  log_puts(text)
  log_puts(terminator)
  nil
end
put_data(name, obj) click to toggle source
# File lib/structured_log.rb, line 168
def put_data(name, obj)
  put_element('data', :name => name, :class => obj.class) do
    put_cdata(obj.inspect)
  end
end
put_each_pair(name, obj) click to toggle source
# File lib/structured_log.rb, line 154
def put_each_pair(name, obj)
  lines = ['']
  obj.each_pair do |key, value|
    lines.push(format('%s => %s', key, value))
  end
  lines.push('')
  lines.push('')
  put_element('each_pair', :name => name, :class => obj.class) do
    put_cdata(lines.join("\n"))
  end
  nil
end
Also aliased as: put_hash
put_each_with_index(name, obj) click to toggle source
# File lib/structured_log.rb, line 139
def put_each_with_index(name, obj)
  lines = ['']
  obj.each_with_index do |item, i|
    lines.push(format('%6d %s', i, item.to_s))
  end
  lines.push('')
  lines.push('')
  put_element('each_with_index', :name => name, :class => obj.class) do
    put_cdata(lines.join("\n"))
  end
  nil
end
Also aliased as: put_array, put_set
put_element(element_name = 'element', *args) { || ... } click to toggle source

Log an XML element.

  • element_name: Element name for logged element.

  • *args: Anything; processed left to right; for each arg:

    • Hash: becomes element attributes.

    • String: appended to PCDATA.

    • :timestamp: causes a timestamp to be added to the element.

    • :duration: causes block's execution duration to be added to the element.

    • :rescue: causes any exception raised during block execution to be rescued and logged.

    • else: arg.inspect is appended to PCDATA.

# File lib/structured_log.rb, line 77
def put_element(element_name = 'element', *args)
  attributes = {}
  pcdata = ''
  start_time = nil
  duration_to_be_included = false
  block_to_be_rescued = false
  args.each do |arg|
    case
      when arg.kind_of?(Hash)
        attributes.merge!(arg)
      when arg.kind_of?(String)
        pcdata += arg
      when arg == :timestamp
        attributes[:timestamp] = StructuredLog.timestamp
      when arg == :duration
        duration_to_be_included = true
      when arg == :rescue
        block_to_be_rescued = true
      else
        pcdata = pcdata + arg.inspect
    end
  end
  log_puts("BEGIN\t#{element_name}")
  put_attributes(attributes)
  unless pcdata.empty?
    # Guard against using a terminator that's a substring of pcdata.
    s = 'EOT'
    terminator = s
    while pcdata.match(terminator) do
      terminator += s
    end
    log_puts("PCDATA\t<<#{terminator}")
    log_puts(pcdata)
    log_puts(terminator)
  end
  start_time = Time.new if duration_to_be_included
  if block_given?
    if block_to_be_rescued
      begin
        yield
      rescue Exception => x
        put_element('rescued_exception', :timestamp, :class => x.class) do
          put_element('message', x.message)
          put_element('backtrace') do
            put_cdata(filter_backtrace(x.backtrace))
          end
        end
      end
    else
      yield
    end
  end
  if start_time
    end_time = Time.now
    duration_f = end_time.to_f - start_time.to_f
    duration_s = format('%.3f', duration_f)
    put_attributes({:duration_seconds => duration_s})
  end
  log_puts("END\t#{element_name}")
  nil
end
put_hash(name, obj)
Alias for: put_each_pair
put_set(name, obj)
Alias for: put_each_with_index
section(name, *args) { || ... } click to toggle source

Start a new section, within the current section. Sections may be nested.

  • name: name for the section.

  • *args: passed to method put_element.

# File lib/structured_log.rb, line 56
def section(name, *args)
  put_element('section', {:name => name}, *args) do
    yield
  end
  nil
end

Private Instance Methods

dispose() click to toggle source
# File lib/structured_log.rb, line 205
def dispose

  # Close the text log.
  log_puts("END\t#{self.root_name}")
  self.file.close

  # Create the xml log.
  document = REXML::Document.new
  File.open(self.file_path, 'r') do |file|
    element = document
    stack = Array.new
    data_a = nil
    terminator = nil
    file.each_line do |line|
      line.chomp!
      line_type, text = line.split("\t", 2)
      case line_type
        when 'REMARK'
          next
        when 'BEGIN'
          element_name = text
          element = element.add_element(element_name)
          stack.push(element)
        when 'END'
          stack.pop
          element = stack.last
        when 'ATTRIBUTE'
          attr_name, attr_value = text.split("\t", 2)
          element.add_attribute(attr_name, attr_value)
        when 'CDATA'
          stack.push(:cdata)
          data_a = Array.new
          terminator = text.split('<<', 2).last
        when 'PCDATA'
          stack.push(:pcdata)
          data_a = Array.new
          terminator = text.split('<<', 2).last
        when terminator
          data_s = data_a.join("\n")
          data_a = nil
          terminator = nil
          data_type = stack.last
          case data_type
            when :cdata
              cdata = CData.new(data_s)
              element.add(cdata)
            when :pcdata
              element.add_text(data_s)
            else
              # Don't want to raise an exception and spoil the run
          end
          stack.pop
        else
          data_a.push(line) if (terminator)
      end
    end
    document << XMLDecl.default
  end

  File.open(self.file_path, 'w') do |file|
    document.write(file, self.xml_indentation)
    file.puts('')
  end
  nil
end
filter_backtrace(lines) click to toggle source

Filters lines, to make the backtrace more readable.

# File lib/structured_log.rb, line 293
def filter_backtrace(lines)
  filtered = []
  lines.each do |line|
    unless line.match(self.backtrace_filter)
      filtered.push(line)
    end
  end
  filtered = lines if filtered.empty?
  filtered.push('')
  filtered.push('')
  filtered.unshift('')
  filtered.join("\n")
end
log_puts(text) click to toggle source
# File lib/structured_log.rb, line 286
def log_puts(text)
  self.file.puts(text)
  self.file.flush
  nil
end
put_attributes(attributes) click to toggle source
# File lib/structured_log.rb, line 271
def put_attributes(attributes)
  attributes.each_pair do |name, value|
    value = case
              when value.is_a?(String)
                value
              when value.is_a?(Symbol)
                value.to_s
              else
                value.inspect
            end
    log_puts("ATTRIBUTE\t#{name}\t#{value}")
  end
  nil
end