diff --git a/lib/puppet/util/log.rb b/lib/puppet/util/log.rb
index ba23e120a..237887eb5 100644
--- a/lib/puppet/util/log.rb
+++ b/lib/puppet/util/log.rb
@@ -1,275 +1,282 @@
require 'puppet/util/tagging'
require 'puppet/util/classgen'
# 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::Tagging
@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)
return dest
end
require 'puppet/util/log/destination'
require 'puppet/util/log/destinations'
@destinations = {}
+ @queued = []
+
class << self
include Puppet::Util
include Puppet::Util::ClassGen
attr_reader :desttypes
end
- # Reset all logs to basics. Basically just closes all files and undefs
- # all of the other objects.
- def Log.close(dest = nil)
- if dest
- if @destinations.include?(dest)
- if @destinations.respond_to?(:close)
- @destinations[dest].close
- end
- @destinations.delete(dest)
+ # Reset log to basics. Basically just flushes and closes files and
+ # undefs other objects.
+ def Log.close(destination)
+ if @destinations.include?(destination)
+ if @destinations[destination].respond_to?(:flush)
+ @destinations[destination].flush
end
- else
- @destinations.each { |name, dest|
- if dest.respond_to?(:flush)
- dest.flush
- end
- if dest.respond_to?(:close)
- dest.close
- end
- }
- @destinations = {}
+ if @destinations[destination].respond_to?(:close)
+ @destinations[destination].close
+ end
+ @destinations.delete(destination)
end
end
def self.close_all
- # And close all logs except the console.
- destinations.each do |dest|
+ destinations.keys.each { |dest|
close(dest)
- end
+ }
end
# Flush any log destinations that support such operations.
def Log.flush
@destinations.each { |type, dest|
if dest.respond_to?(:flush)
dest.flush
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)
unless hash.include?(:level)
raise Puppet::DevError, "Logs require a level"
end
unless @levels.index(hash[:level])
raise Puppet::DevError, "Invalid log level %s" % hash[:level]
end
if @levels.index(hash[:level]) >= @loglevel
return Puppet::Util::Log.new(hash)
else
return nil
end
end
def Log.destinations
- return @destinations.keys
+ @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
return @levels[@loglevel]
end
# Set the current log level.
def Log.level=(level)
unless level.is_a?(Symbol)
level = level.intern
end
unless @levels.include?(level)
raise Puppet::DevError, "Invalid loglevel %s" % level
end
@loglevel = @levels.index(level)
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
name, type = @desttypes.find do |name, klass|
klass.match?(dest)
end
unless type
raise Puppet::DevError, "Unknown destination type %s" % dest
end
begin
if type.instance_method(:initialize).arity == 1
@destinations[dest] = type.new(dest)
else
@destinations[dest] = type.new()
end
+ flushqueue
+ @destinations[dest]
rescue => detail
if Puppet[:debug]
puts detail.backtrace
end
# If this was our only destination, then add the console back in.
if @destinations.empty? and (dest != :console and dest != "console")
newdestination(:console)
end
end
end
# Route the actual message. FIXME There are lots of things this method
- # should do, like caching, storing messages when there are not yet
- # destinations, 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
+ # 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)
if @levels.index(msg.level) < @loglevel
return
end
+ queuemessage(msg) if @destinations.length == 0
+
@destinations.each do |name, dest|
threadlock(dest) do
dest.handle(msg)
end
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
+
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|
if dest.respond_to?(:close)
dest.close
end
}
@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.newdestination(:syslog)
Puppet.err detail.to_s
end
end
end
# Is the passed level a valid log level?
def self.validlevel?(level)
@levels.include?(level)
end
attr_accessor :time, :remote, :file, :line, :version, :source
attr_reader :level, :message
def initialize(args)
self.level = args[:level]
self.message = args[:message]
self.source = args[:source] || "Puppet"
@time = Time.now
if tags = args[:tags]
tags.each { |t| self.tag(t) }
end
[:file, :line, :version].each do |attr|
next unless value = args[attr]
send(attr.to_s + "=", value)
end
Log.newmessage(self)
end
def message=(msg)
raise ArgumentError, "Puppet::Util::Log requires a message" unless msg
@message = msg.to_s
end
def level=(level)
raise ArgumentError, "Puppet::Util::Log requires a log level" unless level
@level = level.to_sym
raise ArgumentError, "Invalid log level %s" % @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 source.respond_to?(:source_descriptors)
descriptors = source.source_descriptors
@source = descriptors[:path]
descriptors[:tags].each { |t| tag(t) }
[:file, :line, :version].each do |param|
next unless descriptors[param]
send(param.to_s + "=", descriptors[param])
end
else
@source = source.to_s
end
end
def to_report
"#{time} #{source} (#{level}): #{to_s}"
end
def to_s
message
end
end
# This is for backward compatibility from when we changed the constant to Puppet::Util::Log
# because the reports include the constant name. Apparently the alias was created in
# March 2007, should could probably be removed soon.
Puppet::Log = Puppet::Util::Log
diff --git a/lib/puppet/util/log/destinations.rb b/lib/puppet/util/log/destinations.rb
index 002ca3624..403733d35 100644
--- a/lib/puppet/util/log/destinations.rb
+++ b/lib/puppet/util/log/destinations.rb
@@ -1,229 +1,234 @@
Puppet::Util::Log.newdesttype :syslog do
def close
Syslog.close
end
def initialize
if Syslog.opened?
Syslog.close
end
name = Puppet[:name]
name = "puppet-#{name}" unless name =~ /puppet/
options = Syslog::LOG_PID | Syslog::LOG_NDELAY
# XXX This should really be configurable.
str = Puppet[:syslogfacility]
begin
facility = Syslog.const_get("LOG_#{str.upcase}")
rescue NameError
raise Puppet::Error, "Invalid syslog facility %s" % str
end
@syslog = Syslog.open(name, options, facility)
end
def handle(msg)
# XXX Syslog currently has a bug that makes it so you
# cannot log a message with a '%' in it. So, we get rid
# of them.
if msg.source == "Puppet"
@syslog.send(msg.level, msg.to_s.gsub("%", '%%'))
else
@syslog.send(msg.level, "(%s) %s" %
[msg.source.to_s.gsub("%", ""),
msg.to_s.gsub("%", '%%')
]
)
end
end
end
Puppet::Util::Log.newdesttype :file do
match(/^\//)
def close
if defined? @file
@file.close
@file = nil
end
end
def flush
if defined? @file
@file.flush
end
end
def initialize(path)
@name = path
# first make sure the directory exists
# We can't just use 'Config.use' here, because they've
# specified a "special" destination.
unless FileTest.exist?(File.dirname(path))
Puppet.recmkdir(File.dirname(path))
Puppet.info "Creating log directory %s" % File.dirname(path)
end
# create the log file, if it doesn't already exist
file = File.open(path, File::WRONLY|File::CREAT|File::APPEND)
@file = file
@autoflush = Puppet[:autoflush]
end
def handle(msg)
@file.puts("%s %s (%s): %s" %
[msg.time, msg.source, msg.level, msg.to_s])
@file.flush if @autoflush
end
end
Puppet::Util::Log.newdesttype :console do
RED = {:console => "[0;31m", :html => "FFA0A0"}
GREEN = {:console => "[0;32m", :html => "00CD00"}
YELLOW = {:console => "[0;33m", :html => "FFFF60"}
BLUE = {:console => "[0;34m", :html => "80A0FF"}
PURPLE = {:console => "[0;35m", :html => "FFA500"}
CYAN = {:console => "[0;36m", :html => "40FFFF"}
WHITE = {:console => "[0;37m", :html => "FFFFFF"}
HRED = {:console => "[1;31m", :html => "FFA0A0"}
HGREEN = {:console => "[1;32m", :html => "00CD00"}
HYELLOW = {:console => "[1;33m", :html => "FFFF60"}
HBLUE = {:console => "[1;34m", :html => "80A0FF"}
HPURPLE = {:console => "[1;35m", :html => "FFA500"}
HCYAN = {:console => "[1;36m", :html => "40FFFF"}
HWHITE = {:console => "[1;37m", :html => "FFFFFF"}
RESET = {:console => "[0m", :html => "" }
@@colormap = {
:debug => WHITE,
:info => GREEN,
:notice => CYAN,
:warning => YELLOW,
:err => HPURPLE,
:alert => RED,
:emerg => HRED,
:crit => HRED
}
def colorize(level, str)
case Puppet[:color]
when true, :ansi, "ansi", "yes"; console_color(level, str)
when :html, "html"; html_color(level, str)
else
str
end
end
def console_color(level, str)
@@colormap[level][:console] + str + RESET[:console]
end
def html_color(level, str)
%{%s} % [@@colormap[level][:html], str]
end
def initialize
# Flush output immediately.
$stdout.sync = true
end
def handle(msg)
if msg.source == "Puppet"
puts colorize(msg.level, "%s: %s" % [msg.level, msg.to_s])
else
puts colorize(msg.level, "%s: %s: %s" % [msg.level, msg.source, msg.to_s])
end
end
end
Puppet::Util::Log.newdesttype :host do
def initialize(host)
Puppet.info "Treating %s as a hostname" % host
args = {}
if host =~ /:(\d+)/
args[:Port] = $1
args[:Server] = host.sub(/:\d+/, '')
else
args[:Server] = host
end
@name = host
@driver = Puppet::Network::Client::LogClient.new(args)
end
def handle(msg)
unless msg.is_a?(String) or msg.remote
unless defined? @hostname
@hostname = Facter["hostname"].value
end
unless defined? @domain
@domain = Facter["domain"].value
if @domain
@hostname += "." + @domain
end
end
if msg.source =~ /^\//
msg.source = @hostname + ":" + msg.source
elsif msg.source == "Puppet"
msg.source = @hostname + " " + msg.source
else
msg.source = @hostname + " " + msg.source
end
begin
#puts "would have sent %s" % msg
#puts "would have sent %s" %
# CGI.escape(YAML.dump(msg))
begin
tmp = CGI.escape(YAML.dump(msg))
rescue => detail
puts "Could not dump: %s" % detail.to_s
return
end
# Add the hostname to the source
@driver.addlog(tmp)
rescue => detail
if Puppet[:trace]
puts detail.backtrace
end
Puppet.err detail
Puppet::Util::Log.close(self)
end
end
end
end
# Log to a transaction report.
Puppet::Util::Log.newdesttype :report do
attr_reader :report
match "Puppet::Transaction::Report"
def initialize(report)
@report = report
end
def handle(msg)
@report << msg
end
end
# Log to an array, just for testing.
Puppet::Util::Log.newdesttype :array do
match "Array"
- def initialize(array)
- @array = array
+ attr_accessor :messages
+ def initialize
+ @messages = []
end
def handle(msg)
- @array << msg
+ @messages << msg
+ end
+
+ def close
+ @messages.clear
end
end
diff --git a/spec/unit/util/log_spec.rb b/spec/unit/util/log_spec.rb
index 7aaa580c5..df3c36ff2 100755
--- a/spec/unit/util/log_spec.rb
+++ b/spec/unit/util/log_spec.rb
@@ -1,209 +1,226 @@
#!/usr/bin/env ruby
Dir.chdir(File.dirname(__FILE__)) { (s = lambda { |f| File.exist?(f) ? require(f) : Dir.chdir("..") { s.call(f) } }).call("spec/spec_helper.rb") }
require 'puppet/util/log'
describe Puppet::Util::Log do
+ it "should write a given message to the specified destination" do
+ Puppet::Util::Log.newdestination(:array)
+ Puppet::Util::Log.new(:level => :notice, :message => "foo")
+ message = Puppet::Util::Log.destinations[:array].messages.shift.message
+ message.should == "foo"
+
+ Puppet::Util::Log.close_all
+ end
+
it "should be able to close all log destinations" do
- Puppet::Util::Log.expects(:destinations).returns %w{foo bar}
+ destinations = stub_everything('destinations')
+ destinations.stubs(:keys).returns %w{foo bar}
+ Puppet::Util::Log.expects(:destinations).returns(destinations)
Puppet::Util::Log.expects(:close).with("foo")
Puppet::Util::Log.expects(:close).with("bar")
Puppet::Util::Log.close_all
end
describe Puppet::Util::Log::DestConsole do
before do
@console = Puppet::Util::Log::DestConsole.new
end
it "should colorize if Puppet[:color] is :ansi" do
Puppet[:color] = :ansi
@console.colorize(:alert, "abc").should == "\e[0;31mabc\e[0m"
end
it "should colorize if Puppet[:color] is 'yes'" do
Puppet[:color] = "yes"
@console.colorize(:alert, "abc").should == "\e[0;31mabc\e[0m"
end
it "should htmlize if Puppet[:color] is :html" do
Puppet[:color] = :html
@console.colorize(:alert, "abc").should == "abc"
end
it "should do nothing if Puppet[:color] is false" do
Puppet[:color] = false
@console.colorize(:alert, "abc").should == "abc"
end
it "should do nothing if Puppet[:color] is invalid" do
Puppet[:color] = "invalid option"
@console.colorize(:alert, "abc").should == "abc"
end
end
describe "instances" do
before do
Puppet::Util::Log.stubs(:newmessage)
end
[:level, :message, :time, :remote].each do |attr|
it "should have a %s attribute" % attr do
log = Puppet::Util::Log.new :level => :notice, :message => "A test message"
log.should respond_to(attr)
log.should respond_to(attr.to_s + "=")
end
end
it "should fail if created without a level" do
lambda { Puppet::Util::Log.new(:message => "A test message") }.should raise_error(ArgumentError)
end
it "should fail if created without a message" do
lambda { Puppet::Util::Log.new(:level => :notice) }.should raise_error(ArgumentError)
end
it "should make available the level passed in at initialization" do
Puppet::Util::Log.new(:level => :notice, :message => "A test message").level.should == :notice
end
it "should make available the message passed in at initialization" do
Puppet::Util::Log.new(:level => :notice, :message => "A test message").message.should == "A test message"
end
# LAK:NOTE I don't know why this behavior is here, I'm just testing what's in the code,
# at least at first.
it "should always convert messages to strings" do
Puppet::Util::Log.new(:level => :notice, :message => :foo).message.should == "foo"
end
+ it "should flush the log queue when the first destination is specified" do
+ Puppet::Util::Log.expects(:flushqueue)
+ Puppet::Util::Log.newdestination(:array)
+ Puppet::Util::Log.close_all
+ end
+
it "should convert the level to a symbol if it's passed in as a string" do
Puppet::Util::Log.new(:level => "notice", :message => :foo).level.should == :notice
end
it "should fail if the level is not a symbol or string" do
lambda { Puppet::Util::Log.new(:level => 50, :message => :foo) }.should raise_error(ArgumentError)
end
it "should fail if the provided level is not valid" do
Puppet::Util::Log.expects(:validlevel?).with(:notice).returns false
lambda { Puppet::Util::Log.new(:level => :notice, :message => :foo) }.should raise_error(ArgumentError)
end
it "should set its time to the initialization time" do
time = mock 'time'
Time.expects(:now).returns time
Puppet::Util::Log.new(:level => "notice", :message => :foo).time.should equal(time)
end
it "should make available any passed-in tags" do
log = Puppet::Util::Log.new(:level => "notice", :message => :foo, :tags => %w{foo bar})
log.tags.should be_include("foo")
log.tags.should be_include("bar")
end
it "should use an passed-in source" do
Puppet::Util::Log.any_instance.expects(:source=).with "foo"
Puppet::Util::Log.new(:level => "notice", :message => :foo, :source => "foo")
end
[:file, :line, :version].each do |attr|
it "should use #{attr} if provided" do
Puppet::Util::Log.any_instance.expects(attr.to_s + "=").with "foo"
Puppet::Util::Log.new(:level => "notice", :message => :foo, attr => "foo")
end
end
it "should default to 'Puppet' as its source" do
Puppet::Util::Log.new(:level => "notice", :message => :foo).source.should == "Puppet"
end
it "should register itself with Log" do
Puppet::Util::Log.expects(:newmessage)
Puppet::Util::Log.new(:level => "notice", :message => :foo)
end
it "should have a method for determining if a tag is present" do
Puppet::Util::Log.new(:level => "notice", :message => :foo).should respond_to(:tagged?)
end
it "should match a tag if any of the tags are equivalent to the passed tag as a string" do
Puppet::Util::Log.new(:level => "notice", :message => :foo, :tags => %w{one two}).should be_tagged(:one)
end
it "should tag itself with its log level" do
Puppet::Util::Log.new(:level => "notice", :message => :foo).should be_tagged(:notice)
end
it "should return its message when converted to a string" do
Puppet::Util::Log.new(:level => "notice", :message => :foo).to_s.should == "foo"
end
it "should include its time, source, level, and message when prepared for reporting" do
log = Puppet::Util::Log.new(:level => "notice", :message => :foo)
report = log.to_report
report.should be_include("notice")
report.should be_include("foo")
report.should be_include(log.source)
report.should be_include(log.time.to_s)
end
describe "when setting the source as a RAL object" do
it "should tag itself with any tags the source has" do
source = Puppet::Type.type(:file).new :path => "/foo/bar"
log = Puppet::Util::Log.new(:level => "notice", :message => :foo, :source => source)
source.tags.each do |tag|
log.tags.should be_include(tag)
end
end
it "should use the source_descriptors" do
source = stub "source"
source.stubs(:source_descriptors).returns(:tags => ["tag","tag2"], :path => "path", :version => 100)
log = Puppet::Util::Log.new(:level => "notice", :message => :foo)
log.expects(:tag).with("tag")
log.expects(:tag).with("tag2")
log.expects(:version=).with(100)
log.source = source
log.source.should == "path"
end
it "should copy over any version information" do
catalog = Puppet::Resource::Catalog.new
catalog.version = 25
source = Puppet::Type.type(:file).new :path => "/foo/bar"
catalog.add_resource source
log = Puppet::Util::Log.new(:level => "notice", :message => :foo, :source => source)
log.version.should == 25
end
it "should copy over any file and line information" do
source = Puppet::Type.type(:file).new :path => "/foo/bar"
source.file = "/my/file"
source.line = 50
log = Puppet::Util::Log.new(:level => "notice", :message => :foo, :source => source)
log.file.should == "/my/file"
log.line.should == 50
end
end
describe "when setting the source as a non-RAL object" do
it "should not try to copy over file, version, line, or tag information" do
source = Puppet::Module.new("foo")
source.expects(:file).never
log = Puppet::Util::Log.new(:level => "notice", :message => :foo, :source => source)
end
end
end
end
diff --git a/spec/unit/util/logging_spec.rb b/spec/unit/util/logging_spec.rb
index aee308eca..41b07d433 100755
--- a/spec/unit/util/logging_spec.rb
+++ b/spec/unit/util/logging_spec.rb
@@ -1,88 +1,94 @@
#!/usr/bin/env ruby
Dir.chdir(File.dirname(__FILE__)) { (s = lambda { |f| File.exist?(f) ? require(f) : Dir.chdir("..") { s.call(f) } }).call("spec/spec_helper.rb") }
require 'puppet/util/logging'
class LoggingTester
include Puppet::Util::Logging
end
describe Puppet::Util::Logging do
before do
@logger = LoggingTester.new
end
Puppet::Util::Log.eachlevel do |level|
it "should have a method for sending '#{level}' logs" do
@logger.should respond_to(level)
end
end
it "should have a method for sending a log with a specified log level" do
@logger.expects(:to_s).returns "I'm a string!"
Puppet::Util::Log.expects(:create).with { |args| args[:source] == "I'm a string!" and args[:level] == "loglevel" and args[:message] == "mymessage" }
@logger.send_log "loglevel", "mymessage"
end
describe "when sending a log" do
it "should use the Log's 'create' entrance method" do
Puppet::Util::Log.expects(:create)
@logger.notice "foo"
end
it "should send itself converted to a string as the log source" do
@logger.expects(:to_s).returns "I'm a string!"
Puppet::Util::Log.expects(:create).with { |args| args[:source] == "I'm a string!" }
@logger.notice "foo"
end
+ it "should queue logs sent without a specified destination" do
+ Puppet::Util::Log.expects(:queuemessage)
+
+ @logger.notice "foo"
+ end
+
it "should use the path of any provided resource type" do
resource = Puppet::Type.type(:mount).new :name => "foo"
resource.expects(:path).returns "/path/to/mount".to_sym
Puppet::Util::Log.expects(:create).with { |args| args[:source] == "/path/to/mount" }
resource.notice "foo"
end
it "should use the path of any provided resource parameter" do
resource = Puppet::Type.type(:mount).new :name => "foo"
param = resource.parameter(:name)
param.expects(:path).returns "/path/to/param".to_sym
Puppet::Util::Log.expects(:create).with { |args| args[:source] == "/path/to/param" }
param.notice "foo"
end
it "should send the provided argument as the log message" do
Puppet::Util::Log.expects(:create).with { |args| args[:message] == "foo" }
@logger.notice "foo"
end
it "should join any provided arguments into a single string for the message" do
Puppet::Util::Log.expects(:create).with { |args| args[:message] == "foo bar baz" }
@logger.notice ["foo", "bar", "baz"]
end
[:file, :line, :version, :tags].each do |attr|
it "should include #{attr} if available" do
@logger.singleton_class.send(:attr_accessor, attr)
@logger.send(attr.to_s + "=", "myval")
Puppet::Util::Log.expects(:create).with { |args| args[attr] == "myval" }
@logger.notice "foo"
end
end
end
end