class DumbLogger

This is just a one-off class to allow reporting things to stderr according to the verbosity level. Very simple, not a complete logger at all.

@todo

Allow assignment of prefices to levels the way we now do labels.
Will probably only work with level-based reporting, since
mask-based reports may get transmitted due to a mask `AND` that
doesn't match any named masks.

@todo

Add a `:seek_to_eof` option so that text written to a sink is
*always* possitioned after any data written by other processes.
(Except on the first write to a file in truncation `:append => false`
mode, of course.)

Constants

CONSTRUCTOR_OPTIONS

@private

List of option keys settable in the constructor.

NO_NL

Message flag for “do not append a newline”.

SPECIAL_SINKS

Special sink values, which will get evaluated on every transmission.

USE_BITMASK

Treat loglevel numbers as bitmasks.

USE_LEVELS

Treat loglevel numbers as actual levels.

VERSION

Frozen string representation of the module version number.

Public Class Methods

VERSION() click to toggle source

Returns the package version number as a string.

@return [String]

Package version number.
# File lib/dumb-logger/version.rb, line 70
def self.VERSION
  return self.const_get('VERSION')
end
finalize(obj) click to toggle source

If we have a currently open output stream that needs to be closed (usually because we opened it ourself), close it as part of the DumbLogger object teardown.

@param [DumbLogger] obj

Instance being torn down ('destructed').
# File lib/dumb-logger.rb, line 53
def finalize(obj)
  if (obj.instance_variable_get(:@options)[:needs_close])
    obj.sink.close
  end
end
new(args={}) click to toggle source

Constructor.

@param [Hash] args @option args [Boolean] :append (true)

If true, any **files** opened will have transmitted text appended to
them.  See {#append=}.

@note

Streams are **always** treated as being in `:append => true` mode.

@option args [String] :prefix ('')

String to insert at the beginning of each line of report text.
See {#prefix=}.

@option args [IO,String] :sink (:$stderr)

Where reports should be sent.  See {#sink=}.

@option args [Integer] :loglevel (0)

Maximum log level for reports.  See {#loglevel=}.

@option args [Integer] :logmask (0)

Alias for `:loglevel`.

@option args [Symbol] :level_style (USE_LEVELS)

Whether message loglevels should be treated as integer levels or
as bitmasks.  See {#level_style=}.

@raise [ArgumentError]

Raises an *ArgumentError* exception if the argument isn't a hash.
# File lib/dumb-logger.rb, line 406
def initialize(args={})
  unless (args.kind_of?(Hash))
    raise ArgumentError.new("#{self.class.name}.new requires a hash")
  end
  @options = {
    :labels           => {},
  }
  #
  # Here are the default settings for a new instance with no
  # arguments.  We put 'em here so they don't show up in docco under
  # the Constants heading.
  #
  default_opts        = {
    :append           => true,
    :level_style      => USE_LEVELS,
    :loglevel         => 0,
    :prefix           => '',
    :sink             => :$stderr,
  }
  #
  # Make a new hash merging the user arguments on top of the
  # defaults.  This avoids altering the user's hash.
  #
  temp_opts           = default_opts.merge(args)
  #
  # Throw out any option keys we don't recognise.
  #
  temp_opts.delete_if { |k,v| (! CONSTRUCTOR_OPTIONS.include?(k)) }
  #
  # Do loglevel stuff.  We're going to run this through the writer
  # method, since it has argument validation code.
  #
  # If the user wants to use bitmasks, then the :logmask argument
  # key takes precedence over the :loglevel one.
  #
  self.level_style = temp_opts[:level_style]
  temp_opts.delete(:level_style)
  if (self.log_masks?)
    temp_opts[:loglevel] = temp_opts[:logmask] if (temp_opts.key?(:logmask))
  end
  temp_opts.delete(:logmask)
  #
  # Now go through the remaining options and handle them.  If the
  # option has an associated writer method, call it -- otherwise,
  # just load it into the `@options` hash.
  #
  temp_opts.each do |opt,val|
    wmethod   = (opt.to_s + '=').to_sym
    if (self.respond_to?(wmethod))
      self.send(wmethod, val)
    else
      @options[opt] = val
    end
  end
end
version() click to toggle source

Returns the {rubygems.org/gems/versionomy Versionomy} representation of the package version number.

@return [Versionomy]

# File lib/dumb-logger/version.rb, line 60
def self.version
  return @version
end

Public Instance Methods

append() click to toggle source

@!attribute [rw] append

Controls the behaviour of sink files (but not IO streams). If `true`, report text will be added to the end of any existing contents; if `false`, files will be truncated and reports will begin at position `0`.

@note

This setting is only important when a sink is being activated,
such as `DumbLogger` object instantiation or because of a call to
{#sink=}, and it controls the position of the first write to the
sink.  Once a sink is activated (opened), writing continues
sequentially from that point.

@note

Setting this attribute *only* affects **files** opened by
`DumbLogger`.  Stream sinks are *always* in append-mode.  As long
as the sink is a stream, this setting will be ignored -- but it
will become active whenever the sink becomes a file.

@return [Boolean]

Sets or returns the file append-on-write control value.
# File lib/dumb-logger.rb, line 486
def append
  return (@options[:append] ? true : false)
end
append=(arg) click to toggle source
# File lib/dumb-logger.rb, line 490
def append=(arg)
  @options[:append]   = (arg ? true : false)
  return self.append
end
append?() click to toggle source

@return [Boolean]

Returns `true` if new sink files opened by the instance will have
report text appended to them.
# File lib/dumb-logger.rb, line 500
def append?
  return self.append
end
label_levels(labelhash) click to toggle source

Allow the user to assign labels to different log levels or mask combinations. All labels will be downcased and converted to symbols.

In addition, the labels are added to the instance as methods that will log messages with the specified level.

@see labeled_levels

@param [Hash{String,Symbol=>Integer}] labelhash

Hash of names or symbols and the integer log levels/masks they're
labelling.

@return [Hash<Symbol,Integer>]

Returns a hash of the labels (as symbols) and levels/masks that
have been assigned.

@raise [ArgumentError]

Raises an *ArgumentError* exception if the argument isn't a hash
with integer values.
# File lib/dumb-logger.rb, line 199
def label_levels(labelhash)
  unless (labelhash.kind_of?(Hash))
    raise ArgumentError.new('level labels must be supplied as a hash')
  end
  unless (labelhash.values.all? { |o| o.kind_of?(Integer) })
    raise ArgumentError.new('labeled levels must be integers')
  end
  newhash = labelhash.inject({}) { |memo,(label,level)|
    label_sym = label.to_s.downcase.to_sym
    memo[label_sym] = level
    memo
  }
  @options[:labels].merge!(newhash)
  newhash.each do |label,level|
    self.define_singleton_method(label) do |*args|
      (scratch, newargs) = args.partition { |o| o.kind_of?(Integer) }
      return self.message(level, *newargs)
    end
  end
  return newhash
end
labeled_levels() click to toggle source

Return a list of all the levels or bitmasks that have been labeled. The return value is suitable for use as input to the label_levels method of this or another instance of this class.

@see label_levels

@return [Hash<Symbol,Integer>]

Returns a hash of labels (as symbols) and the log levels they
identify.
# File lib/dumb-logger.rb, line 232
def labeled_levels
  return Hash[@options[:labels].sort].freeze
end
level_style() click to toggle source

@!attribute [rw] level_style

Control whether loglevels are treated as ascending integers, or as bitmasks.

@return [Symbol]

Returns the current setting (either {USE_LEVELS} or {USE_BITMASK}).

@raise [ArgumentError]

Raises an *ArgumentError* exception if the style isn't
recognised.
# File lib/dumb-logger.rb, line 96
def level_style
  return @options[:level_style]
end
level_style=(style) click to toggle source
# File lib/dumb-logger.rb, line 100
def level_style=(style)
  unless ([ USE_LEVELS, USE_BITMASK ].include?(style))
    raise ArgumentError.new('invalid loglevel style')
  end
  @options[:level_style] = style
end
log_levels?() click to toggle source

Returns `true` if loglevel numbers are interpreted as integers rather than bitmasks. (See {#level_style} for more information.)

@return [Boolean]

Returns `true` if loglevels are regarded as integers rather than
bitmasks, or `false` otherwise.

@see log_masks? @see level_style

# File lib/dumb-logger.rb, line 153
def log_levels?
  return (@options[:level_style] == USE_LEVELS) ? true : false
end
log_masks?() click to toggle source

Returns `true` if loglevel numbers are interpreted as bitmasks rather than integers. (See {#level_style} for more information.)

Determine how loglevel numbers are interpreted. (See {#level_style} for more information.)

Returns `true` if they're treated as bitmasks rather than integers.

@return [Boolean]

Returns `true` if loglevels are regarded as bitmasks rather than
integers, or `false` otherwise.

@see log_levels? @see level_style

# File lib/dumb-logger.rb, line 173
def log_masks?
  return (@options[:level_style] == USE_BITMASK) ? true : false
end
loglevel() click to toggle source
# File lib/dumb-logger.rb, line 137
def loglevel
  return @options[:loglevel].to_i
end
Also aliased as: logmask
loglevel=(arg) click to toggle source

@!attribute [rw] loglevel

If loglevels are being treated as integers, this is the maximum level that will reported; that is, if a message is submitted with level 7, but the loglevel is 5, the message will not be reported.

If loglevels are being treated as bitmasks, messages will be reported only if submitted with a loglevel which has at least one bit set that is also set in the instance loglevel.

When used as an attribute writer (e.g., `obj.loglevel = val`), the argument will be treated as an integer.

@return [Integer]

Returns the maximum loglevel/logging mask in effect henceforth.

@raise [ArgumentError]

Raise an *ArgumentError* exception if the new value cannot be
converted to an integer.
# File lib/dumb-logger.rb, line 128
def loglevel=(arg)
  unless (arg.respond_to?(:to_i))
    raise ArgumentError.new('loglevels are integers')
  end
  @options[:loglevel] = arg.to_i
  return @options[:loglevel]
end
Also aliased as: logmask=
logmask()
Alias for: loglevel
logmask=(arg)
Alias for: loglevel=
message(*args) click to toggle source

Submit a message for possible transmission to the current sink. The argument is an array of arrays, strings, integers, and/or symbols. Reports with a loglevel of zero (the default) are always transmitted.

@param [Array<Array,String,Symbol,Integer,Hash>] args

* The last integer in the array will be treated as the report's
  loglevel; default is `0`.

  **Overridden by `:level` or `:mask` in an options hash passed
  to the method.**
* Any `Array` elements in the arguments will be merged and the
  values interpreted as level labels (see {#label_levels}).  If
  loglevels are bitmasks (see {#level_style}), the labeled levels
  are `OR`ed together; otherwise the lowest labeled level will be
  used for the message.

  **Overridden by `:level` or `:mask` in an options hash passed
  to the method.**
* Any `Hash` elements in the array will be merged and will
  temporarily override instance-wide options -- *e.g.*,
  `{ :prefix  => 'alt' }` .  Valid *per*-call options are:
  * `:prefix  => String`
  * `:level   => Integer`
    (takes precedence over `:mask` if {#level_style} is {USE_LEVELS}.)
  * `:mask    => Integer`
    (takes precedence over `:level` if {#level_style} is {USE_BITMASK}.)
  * `:newline => Boolean`
    (takes precedence over {DumbLogger::NO_NL} in the argument list)
  * `:return  => Boolean`
    (alias for `:newline`; **deprecated after 1.0.0**)
* If the {DumbLogger::NO_NL} value (a `Symbol`) appears in the
  array, or a hash element of `:newline => false` (or `:return =>
  false`), the report will not include a terminating newline
  (useful for `"progress:..done"` reports).
* Any strings in the array are treated as text to be reported,
  one *per* line.  Each line will begin with the value of
  logger's value of {#prefix} (or any overriding value set with
  `:prefix` in a hash of options), and only the final line is
  subject to the {DumbLogger::NO_NL} special-casing.

@note

Use of the `:return` hash option is deprecated in versions after
1.0.0.  Use `:newline` instead.

@return [nil,Integer]

Returns either `nil` if the message's loglevel is higher than the
reporting level, or the level of the report.

If integer levels are being used, a non-`nil` return value is
that of the message.  If bitmask levels are being used, the
return value is a mask of the active level bits that applied to
the message -- *i.e.*, `message_mask & logging_mask` .
# File lib/dumb-logger.rb, line 559
def message(*args)
  #
  # Extract any symbols, hashes, and integers from the argument
  # list.  This makes the calling format very flexible.
  #
  (symopts, args)     = args.partition { |elt| elt.kind_of?(Symbol) }
  #
  # Pull out any symbols that are actually names for levels (or
  # masks).  The args variable now contains no Symbol elements.
  #
  symlevels           = (symopts & self.labeled_levels.keys).map { |o|
    self.labeled_levels[o]
  }.compact
  #
  # Now any option hashes.
  #
  (hashopts, args)    = args.partition { |elt| elt.kind_of?(Hash) }
  hashopts            = hashopts.reduce(:merge) || {}
  #
  # All hashes have been removed from the args array, and merged
  # together into a single *per*-message options hash.
  #

  #
  # Now some fun stuff.  The appropriate loglevel/mask for this
  # message can come from
  #
  # * Integers in the argument array (last one takes precedence); or
  # * Values of symbolic level/mask labels (again, last one takes
  #   precedence, and overrides any explicit integers); or
  # * Any `:level` or `:mask` value in the options hash (which one
  #   of those takes precedence depends on the current logging
  #   style).
  #
  (lvls, args)        = args.partition { |elt| elt.kind_of?(Integer) }
  if (self.log_levels?)
    level             = hashopts[:level] || hashopts[:mask]
  else
    level             = hashopts[:mask] || hashopts[:level]
  end
  if (level.nil?)
    if (self.log_levels?)
      level           = symlevels.empty? ? lvls.last : symlevels.min
    else
      level           = symlevels.empty? ? lvls.last : symlevels.reduce(:|)
    end
  end
  level               ||= 0
  #
  # We should now have a minimum logging level, or an ORed bitmask,
  # in variable 'level'.  Time to see if it meets our criteria.
  #
  unless (level.zero?)
    if (self.log_levels?)
      return nil if (self.loglevel < level)
    elsif (self.log_masks?)
      level &=  self.logmask
      return nil if (level.zero?)
    end
  end
  #
  # Looks like the request loglevel/mask is within the logger's
  # requirements, so let's build the output string.
  #
  prefix_text         = hashopts[:prefix] || self.prefix
  text                = prefix_text + args.join("\n#{prefix_text}")
  #
  # The :return option is overridden by :newline, and renamed to it
  # if :newline isn't already in the options hash.
  #
  if (hashopts.key?(:return) && (! hashopts.key?(:newline)))
    hashopts[:newline] = hashopts[:return]
    hashopts.delete(:return)
  end
  unless (hashopts.key?(:newline))
    hashopts[:newline]= (! symopts.include?(NO_NL))
  end
  text << "\n" if (hashopts[:newline])
  #
  # Okey.  If the output stream is marked 'volatile', it's one of
  # our special sinks and we need to evaluate it on every write.
  #
  stream = @options[:volatile] ? eval(self.sink.to_s) : @sink_io
  #
  # If this is our first write to this sink, make sure we position
  # properly before writing!
  #
  if (@options[:needs_seek] && stream.respond_to?(:seek))
    poz       = (self.append? \
                 ? IO::SEEK_END \
                 : IO::SEEK_SET)
    begin
      #
      # Can't seek on some things, so just catch the exception and
      # ignore it.
      #
      stream.seek(0, poz)
    rescue Errno::ESPIPE => exc
      #
      # Do nothing..
      #
    end
    @options[:needs_seek] = false
  end
  stream.write(text)
  stream.flush if (@options[:volatile])
  #
  # All done!  Return the level, or mask bits, that resulted in the
  # text being transmitted.
  #
  return level
end
options() click to toggle source

@!attribute [r] options

Options controlling various aspects of `DumbLogger`'s operation.

@return [Hash]

Returns current set of DumbLogger options for the instance.
# File lib/dumb-logger.rb, line 258
def options
  return @options.dup.freeze
end
prefix() click to toggle source

@!attribute [rw] prefix

Prefix string to be inserted at the beginning of each line of output we emit.

@note

This can be overridden at runtime *via* the `:prefix` option hash
element to the {#message} method (*q.v.*).

@return [String]

Sets or returns the prefix string to be used henceforth.
# File lib/dumb-logger.rb, line 275
def prefix
  return @options[:prefix]
end
prefix=(arg) click to toggle source
# File lib/dumb-logger.rb, line 279
def prefix=(arg)
  @options[:prefix] = arg.to_s
  return self.prefix
end
reopen() click to toggle source

Re-open the current sink (unless it's a stream). This may be useful if you want to stop and truncate in the middle of logging (by changing the {#append=} option), or something.

@return [Boolean]

Returns `true` if the sink was successfully re-opened, or `false`
otherwise (such as if it's a stream).

@raise [IOError]

Raises an *IOError* exception if the sink stream is already
closed.
# File lib/dumb-logger.rb, line 297
def reopen
  return false unless (@options[:needs_close] && self.sink.kind_of?(String))
  raise IOError.new('sink stream is already closed') if (@sink_io.closed?)
  @sink_io.reopen(self.sink, (self.append? ? 'a' : 'w'))
  @sink_io.sync = true if (@sink_io.respond_to?(:sync=))
  return true
end
sink() click to toggle source

@!attribute [rw] sink

Sets or returns the sink to which we send our messages.

When setting the sink, the value can be an IO instance, a special symbol, or a string. If a string, the `:append` flag from the instance options (see {#append=} and {#append?}) is used to determine whether the file will be rewritten from the beginning, or just have text appended to it.

Sinking to one of the special symbols (`:$stderr` or `:$stdout`; see {SPECIAL_SINKS}) results in the sink being re-evaluated at each call to {#message}. This is useful if these streams might be altered after the logger has been instantiated.

@note

File sink contents may appear unpredictable under the following
conditions:
* Messages are being sinked to a file, **and**
* the file is being accessed by one or more other processes, **and**
* changes to the file are interleaved between those made by the
  `DumbLogger` {#message} method and activity by the other
  process(es).

@return [IO,String,Symbol]

Returns the sink path, special name, or IO object.
# File lib/dumb-logger.rb, line 333
def sink
  return @options[:sink]
end
sink=(arg) click to toggle source
# File lib/dumb-logger.rb, line 337
def sink=(arg)
  if (@options[:needs_close] \
      && @sink_io.respond_to?(:close) \
      && (! [ self.sink, @sink_io ].include?(arg)))
    @sink_io.close unless (@sink_io.closed?)
    @sink_io = nil
  end

  @options[:volatile] = false
  if (arg.kind_of?(IO))
    #
    # If it's an IO, then we assume it's already open.
    #
    @options[:sink] = @sink_io = arg
    @options[:needs_close] = false
  elsif (SPECIAL_SINKS.include?(arg))
    #
    # If it's one of our special symbols, we don't actually do
    # anything except record the fact -- because they get
    # interpreted at each #message call.
    #
    @options[:sink] = arg
    @sink_io = nil
    @options[:volatile] = true
  else
    #
    # If it's a string, we treat it as a file name, open it, and
    # flag it for closing later.
    #
    @options[:sink] = arg
    @sink_io = File.open(@options[:sink], (self.append? ? 'a' : 'w'))
    @options[:needs_close] = true
  end
  #
  # Note that you cannot seek-position the $stdout or $stderr
  # streams.  However, there doesn't seem to be a clear way to
  # determine that, so we'll wrap the actual seek (in {#message}) in
  # a rescue block.
  #
  @options[:needs_seek] = true
  @sink_io.sync = true if (@sink_io.respond_to?(:sync=))
  return self.sink
end