Current File : //opt/puppetlabs/puppet/lib/ruby/vendor_ruby/puppet/util/log.rb |
require_relative '../../puppet/util/tagging'
require_relative '../../puppet/util/classgen'
require_relative '../../puppet/util/psych_support'
require_relative '../../puppet/network/format_support'
# Pass feedback to the user. Log levels are modeled after syslog's, and it is
# expected that that will be the most common log destination. Supports
# multiple destinations, one of which is a remote server.
class Puppet::Util::Log
include Puppet::Util
extend Puppet::Util::ClassGen
include Puppet::Util::PsychSupport
include Puppet::Util::Tagging
include Puppet::Network::FormatSupport
@levels = [:debug,:info,:notice,:warning,:err,:alert,:emerg,:crit]
@loglevel = 2
@desttypes = {}
# Create a new destination type.
def self.newdesttype(name, options = {}, &block)
dest = genclass(
name,
:parent => Puppet::Util::Log::Destination,
:prefix => "Dest",
:block => block,
:hash => @desttypes,
:attributes => options
)
dest.match(dest.name)
dest
end
require_relative 'log/destination'
require_relative 'log/destinations'
@destinations = {}
@queued = []
class << self
include Puppet::Util
include Puppet::Util::ClassGen
attr_reader :desttypes
end
# Reset log to basics. Basically just flushes and closes files and
# undefs other objects.
def Log.close(destination)
if @destinations.include?(destination)
@destinations[destination].flush if @destinations[destination].respond_to?(:flush)
@destinations[destination].close if @destinations[destination].respond_to?(:close)
@destinations.delete(destination)
end
end
def self.close_all
@destinations.keys.each { |dest|
close(dest)
}
#TRANSLATORS "Log.close_all" is a method name and should not be translated
raise Puppet::DevError.new(_("Log.close_all failed to close %{destinations}") % { destinations: @destinations.keys.inspect }) if !@destinations.empty?
end
# Flush any log destinations that support such operations.
def Log.flush
@destinations.each { |type, dest|
dest.flush if dest.respond_to?(:flush)
}
end
def Log.autoflush=(v)
@destinations.each do |type, dest|
dest.autoflush = v if dest.respond_to?(:autoflush=)
end
end
# Create a new log message. The primary role of this method is to
# avoid creating log messages below the loglevel.
def Log.create(hash)
raise Puppet::DevError, _("Logs require a level") unless hash.include?(:level)
raise Puppet::DevError, _("Invalid log level %{level}") % { level: hash[:level] } unless @levels.index(hash[:level])
@levels.index(hash[:level]) >= @loglevel ? Puppet::Util::Log.new(hash) : nil
end
def Log.destinations
@destinations
end
# Yield each valid level in turn
def Log.eachlevel
@levels.each { |level| yield level }
end
# Return the current log level.
def Log.level
@levels[@loglevel]
end
# Set the current log level.
def Log.level=(level)
level = level.intern unless level.is_a?(Symbol)
# loglevel is a 0-based index
loglevel = @levels.index(level)
raise Puppet::DevError, _("Invalid loglevel %{level}") % { level: level } unless loglevel
return if @loglevel == loglevel
# loglevel changed
@loglevel = loglevel
# Enable or disable Facter debugging
Puppet.runtime[:facter].debugging(level == :debug)
end
def Log.levels
@levels.dup
end
# Create a new log destination.
def Log.newdestination(dest)
# Each destination can only occur once.
if @destinations.find { |name, obj| obj.name == dest }
return
end
_, type = @desttypes.find do |name, klass|
klass.match?(dest)
end
if type.respond_to?(:suitable?) and not type.suitable?(dest)
return
end
raise Puppet::DevError, _("Unknown destination type %{dest}") % { dest: dest} unless type
begin
if type.instance_method(:initialize).arity == 1
@destinations[dest] = type.new(dest)
else
@destinations[dest] = type.new
end
flushqueue
@destinations[dest]
rescue => detail
Puppet.log_exception(detail)
# If this was our only destination, then add the console back in.
if destinations.empty? && dest.intern != :console
newdestination(:console)
end
# Re-raise (end exit Puppet) because we could not set up logging correctly.
raise detail
end
end
def Log.with_destination(destination, &block)
if @destinations.include?(destination)
yield
else
newdestination(destination)
begin
yield
ensure
close(destination)
end
end
end
def Log.coerce_string(str)
return Puppet::Util::CharacterEncoding.convert_to_utf_8(str) if str.valid_encoding?
# We only select the last 10 callers in the stack to avoid being spammy
message = _("Received a Log attribute with invalid encoding:%{log_message}") %
{ log_message: Puppet::Util::CharacterEncoding.convert_to_utf_8(str.dump)}
message += '\n' + _("Backtrace:\n%{backtrace}") % { backtrace: caller(1, 10).join("\n") }
message
end
private_class_method :coerce_string
# Route the actual message. FIXME There are lots of things this method
# should do, like caching and a bit more. It's worth noting that there's
# a potential for a loop here, if the machine somehow gets the destination set as
# itself.
def Log.newmessage(msg)
return if @levels.index(msg.level) < @loglevel
msg.message = coerce_string(msg.message)
msg.source = coerce_string(msg.source)
queuemessage(msg) if @destinations.length == 0
@destinations.each do |name, dest|
dest.handle(msg)
end
end
def Log.queuemessage(msg)
@queued.push(msg)
end
def Log.flushqueue
return unless @destinations.size >= 1
@queued.each do |msg|
Log.newmessage(msg)
end
@queued.clear
end
# Flush the logging queue. If there are no destinations available,
# adds in a console logger before flushing the queue.
# This is mainly intended to be used as a last-resort attempt
# to ensure that logging messages are not thrown away before
# the program is about to exit--most likely in a horrific
# error scenario.
# @return nil
def Log.force_flushqueue()
if (@destinations.empty? and !(@queued.empty?))
newdestination(:console)
end
flushqueue
end
def Log.sendlevel?(level)
@levels.index(level) >= @loglevel
end
# Reopen all of our logs.
def Log.reopen
Puppet.notice _("Reopening log files")
types = @destinations.keys
@destinations.each { |type, dest|
dest.close if dest.respond_to?(:close)
}
@destinations.clear
# We need to make sure we always end up with some kind of destination
begin
types.each { |type|
Log.newdestination(type)
}
rescue => detail
if @destinations.empty?
Log.setup_default
Puppet.err detail.to_s
end
end
end
def self.setup_default
Log.newdestination(
(Puppet.features.syslog? ? :syslog :
(Puppet.features.eventlog? ? :eventlog : Puppet[:puppetdlog])))
end
# Is the passed level a valid log level?
def self.validlevel?(level)
@levels.include?(level)
end
def self.from_data_hash(data)
obj = allocate
obj.initialize_from_hash(data)
obj
end
# Log output using scope and level
#
# @param [Puppet::Parser::Scope] scope
# @param [Symbol] level log level
# @param [Array<Object>] vals the values to log (will be converted to string and joined with space)
#
def self.log_func(scope, level, vals)
# NOTE: 3x, does this: vals.join(" ")
# New implementation uses the evaluator to get proper formatting per type
vals = vals.map { |v| Puppet::Pops::Evaluator::EvaluatorImpl.new.string(v, scope) }
# Bypass Puppet.<level> call since it picks up source from "self" which is not applicable in the 4x
# Function API.
# TODO: When a function can obtain the file, line, pos of the call merge those in (3x supports
# options :file, :line. (These were never output when calling the 3x logging functions since
# 3x scope does not know about the calling location at that detailed level, nor do they
# appear in a report to stdout/error when included). Now, the output simply uses scope (like 3x)
# as this is good enough, but does not reflect the true call-stack, but is a rough estimate
# of where the logging call originates from).
#
Puppet::Util::Log.create({:level => level, :source => scope, :message => vals.join(" ")})
nil
end
attr_accessor :time, :remote, :file, :line, :pos, :issue_code, :environment, :node, :backtrace
attr_reader :level, :message, :source
def initialize(args)
self.level = args[:level]
self.message = args[:message]
self.source = args[:source] || "Puppet"
@time = Time.now
tags = args[:tags]
if tags
tags.each { |t| self.tag(t) }
end
# Don't add these unless defined (preserve 3.x API as much as possible)
[:file, :line, :pos, :issue_code, :environment, :node, :backtrace].each do |attr|
value = args[attr]
next unless value
send(attr.to_s + '=', value)
end
Log.newmessage(self)
end
def initialize_from_hash(data)
@level = data['level'].intern
@message = data['message']
@source = data['source']
@tags = Puppet::Util::TagSet.new(data['tags'])
@time = data['time']
if @time.is_a? String
@time = Time.parse(@time)
end
# Don't add these unless defined (preserve 3.x API as much as possible)
%w(file line pos issue_code environment node backtrace).each do |name|
value = data[name]
next unless value
send(name + '=', value)
end
end
def to_hash
self.to_data_hash
end
def to_data_hash
{
'level' => @level.to_s,
'message' => to_s,
'source' => @source,
'tags' => @tags.to_a,
'time' => @time.iso8601(9),
'file' => @file,
'line' => @line,
}
end
def to_structured_hash
hash = {
'level' => @level,
'message' => @message,
'source' => @source,
'tags' => @tags.to_a,
'time' => @time.iso8601(9),
}
%w(file line pos issue_code environment node backtrace).each do |name|
attr_name = "@#{name}"
hash[name] = instance_variable_get(attr_name) if instance_variable_defined?(attr_name)
end
hash
end
def message=(msg)
#TRANSLATORS 'Puppet::Util::Log' refers to a Puppet source code class
raise ArgumentError, _("Puppet::Util::Log requires a message") unless msg
@message = msg.to_s
end
def level=(level)
#TRANSLATORS 'Puppet::Util::Log' refers to a Puppet source code class
raise ArgumentError, _("Puppet::Util::Log requires a log level") unless level
#TRANSLATORS 'Puppet::Util::Log' refers to a Puppet source code class
raise ArgumentError, _("Puppet::Util::Log requires a symbol or string") unless level.respond_to? "to_sym"
@level = level.to_sym
raise ArgumentError, _("Invalid log level %{level}") % { level: @level } unless self.class.validlevel?(@level)
# Tag myself with my log level
tag(level)
end
# If they pass a source in to us, we make sure it is a string, and
# we retrieve any tags we can.
def source=(source)
if defined?(Puppet::Type) && source.is_a?(Puppet::Type)
@source = source.path
merge_tags_from(source)
self.file = source.file
self.line = source.line
else
@source = source.to_s
end
end
def to_report
"#{time} #{source} (#{level}): #{self}"
end
def to_s
msg = message
# Issue based messages do not have details in the message. It
# must be appended here
unless issue_code.nil?
msg = _("Could not parse for environment %{environment}: %{msg}") % { environment: environment, msg: msg } unless environment.nil?
msg += Puppet::Util::Errors.error_location_with_space(file, line, pos)
msg = _("%{msg} on node %{node}") % { msg: msg, node: node } unless node.nil?
if @backtrace.is_a?(Array)
msg += "\n"
msg += @backtrace.join("\n")
end
end
msg
end
end
# This is for backward compatibility from when we changed the constant to
# Puppet::Util::Log because the reports include the constant name. It was
# considered for removal but left in due to risk of breakage (PUP-7502).
Puppet::Log = Puppet::Util::Log