diff --git a/lib/puppet/util/colors.rb b/lib/puppet/util/colors.rb new file mode 100644 index 000000000..05fd58823 --- /dev/null +++ b/lib/puppet/util/colors.rb @@ -0,0 +1,98 @@ +module Puppet::Util::Colors + BLACK = {:console => "\e[0;30m", :html => "color: #FFA0A0" } + RED = {:console => "\e[0;31m", :html => "color: #FFA0A0" } + GREEN = {:console => "\e[0;32m", :html => "color: #00CD00" } + YELLOW = {:console => "\e[0;33m", :html => "color: #FFFF60" } + BLUE = {:console => "\e[0;34m", :html => "color: #80A0FF" } + PURPLE = {:console => "\e[0;35m", :html => "color: #FFA500" } + CYAN = {:console => "\e[0;36m", :html => "color: #40FFFF" } + WHITE = {:console => "\e[0;37m", :html => "color: #FFFFFF" } + HBLACK = {:console => "\e[1;30m", :html => "color: #FFA0A0" } + HRED = {:console => "\e[1;31m", :html => "color: #FFA0A0" } + HGREEN = {:console => "\e[1;32m", :html => "color: #00CD00" } + HYELLOW = {:console => "\e[1;33m", :html => "color: #FFFF60" } + HBLUE = {:console => "\e[1;34m", :html => "color: #80A0FF" } + HPURPLE = {:console => "\e[1;35m", :html => "color: #FFA500" } + HCYAN = {:console => "\e[1;36m", :html => "color: #40FFFF" } + HWHITE = {:console => "\e[1;37m", :html => "color: #FFFFFF" } + BG_RED = {:console => "\e[0;41m", :html => "background: #FFA0A0"} + BG_GREEN = {:console => "\e[0;42m", :html => "background: #00CD00"} + BG_YELLOW = {:console => "\e[0;43m", :html => "background: #FFFF60"} + BG_BLUE = {:console => "\e[0;44m", :html => "background: #80A0FF"} + BG_PURPLE = {:console => "\e[0;45m", :html => "background: #FFA500"} + BG_CYAN = {:console => "\e[0;46m", :html => "background: #40FFFF"} + BG_WHITE = {:console => "\e[0;47m", :html => "background: #FFFFFF"} + BG_HRED = {:console => "\e[1;41m", :html => "background: #FFA0A0"} + BG_HGREEN = {:console => "\e[1;42m", :html => "background: #00CD00"} + BG_HYELLOW = {:console => "\e[1;43m", :html => "background: #FFFF60"} + BG_HBLUE = {:console => "\e[1;44m", :html => "background: #80A0FF"} + BG_HPURPLE = {:console => "\e[1;45m", :html => "background: #FFA500"} + BG_HCYAN = {:console => "\e[1;46m", :html => "background: #40FFFF"} + BG_HWHITE = {:console => "\e[1;47m", :html => "background: #FFFFFF"} + RESET = {:console => "\e[0m", :html => "" } + + Colormap = { + :debug => WHITE, + :info => GREEN, + :notice => CYAN, + :warning => YELLOW, + :err => HPURPLE, + :alert => RED, + :emerg => HRED, + :crit => HRED, + + :black => BLACK, + :red => RED, + :green => GREEN, + :yellow => YELLOW, + :blue => BLUE, + :purple => PURPLE, + :cyan => CYAN, + :white => WHITE, + :hblack => HBLACK, + :hred => HRED, + :hgreen => HGREEN, + :hyellow => HYELLOW, + :hblue => HBLUE, + :hpurple => HPURPLE, + :hcyan => HCYAN, + :hwhite => HWHITE, + :bg_red => BG_RED, + :bg_green => BG_GREEN, + :bg_yellow => BG_YELLOW, + :bg_blue => BG_BLUE, + :bg_purple => BG_PURPLE, + :bg_cyan => BG_CYAN, + :bg_white => BG_WHITE, + :bg_hred => BG_HRED, + :bg_hgreen => BG_HGREEN, + :bg_hyellow => BG_HYELLOW, + :bg_hblue => BG_HBLUE, + :bg_hpurple => BG_HPURPLE, + :bg_hcyan => BG_HCYAN, + :bg_hwhite => BG_HWHITE, + :reset => { :console => "\e[m", :html => "" } + } + + def colorize(color, str) + case Puppet[:color] + when true, :ansi, "ansi", "yes" + console_color(color, str) + when :html, "html" + html_color(color, str) + else + str + end + end + + def console_color(color, str) + Colormap[color][:console] + + str.gsub(RESET[:console], Colormap[color][:console]) + + RESET[:console] + end + + def html_color(color, str) + span = '' % Colormap[color][:html] + "#{span}%s" % str.gsub(//, "\\0#{span}") + end +end diff --git a/lib/puppet/util/log/destinations.rb b/lib/puppet/util/log/destinations.rb index 0ba036cef..607e112cf 100644 --- a/lib/puppet/util/log/destinations.rb +++ b/lib/puppet/util/log/destinations.rb @@ -1,239 +1,230 @@ Puppet::Util::Log.newdesttype :syslog do def self.suitable?(obj) Puppet.features.syslog? end def close Syslog.close end def initialize Syslog.close if Syslog.opened? 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 #{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 require 'fileutils' def self.match?(obj) Puppet::Util.absolute_path?(obj) end def close if defined?(@file) @file.close @file = nil end end def flush @file.flush if defined?(@file) end attr_accessor :autoflush 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)) FileUtils.mkdir_p(File.dirname(path), :mode => 0755) Puppet.info "Creating log directory #{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("#{msg.time} #{msg.source} (#{msg.level}): #{msg}") @file.flush if @autoflush end end Puppet::Util::Log.newdesttype :console do + require 'puppet/util/colors' + include Puppet::Util::Colors + def initialize + # Flush output immediately. + $stdout.sync = true + end - RED = {:console => "", :html => "FFA0A0"} - GREEN = {:console => "", :html => "00CD00"} - YELLOW = {:console => "", :html => "FFFF60"} - BLUE = {:console => "", :html => "80A0FF"} - PURPLE = {:console => "", :html => "FFA500"} - CYAN = {:console => "", :html => "40FFFF"} - WHITE = {:console => "", :html => "FFFFFF"} - HRED = {:console => "", :html => "FFA0A0"} - HGREEN = {:console => "", :html => "00CD00"} - HYELLOW = {:console => "", :html => "FFFF60"} - HBLUE = {:console => "", :html => "80A0FF"} - HPURPLE = {:console => "", :html => "FFA500"} - HCYAN = {:console => "", :html => "40FFFF"} - HWHITE = {:console => "", :html => "FFFFFF"} - RESET = {:console => "", :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) + def handle(msg) + if msg.source == "Puppet" + puts colorize(msg.level, "#{msg.level}: #{msg}") else - str + puts colorize(msg.level, "#{msg.level}: #{msg.source}: #{msg}") end 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 +Puppet::Util::Log.newdesttype :telly_prototype_console do + require 'puppet/util/colors' + include Puppet::Util::Colors def initialize # Flush output immediately. + $stderr.sync = true $stdout.sync = true end def handle(msg) - if msg.source == "Puppet" - puts colorize(msg.level, "#{msg.level}: #{msg}") + error_levels = { + :warning => 'Warning', + :err => 'Error', + :alert => 'Alert', + :emerg => 'Emergency', + :crit => 'Critical' + } + + str = msg.respond_to?(:multiline) ? msg.multiline : msg.to_s + + case msg.level + when *error_levels.keys + $stderr.puts colorize(:hred, "#{error_levels[msg.level]}: #{str}") + when :info + $stdout.puts "#{colorize(:green, 'Info')}: #{str}" + when :debug + $stdout.puts "#{colorize(:cyan, 'Debug')}: #{str}" else - puts colorize(msg.level, "#{msg.level}: #{msg.source}: #{msg}") + $stdout.puts str end end end Puppet::Util::Log.newdesttype :host do def initialize(host) Puppet.info "Treating #{host} as a hostname" 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 @hostname ||= Facter["hostname"].value unless defined?(@domain) @domain = Facter["domain"].value @hostname += ".#{@domain}" if @domain end if Puppet::Util.absolute_path?(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 #{msg}" #puts "would have sent %s" % # CGI.escape(YAML.dump(msg)) begin tmp = CGI.escape(YAML.dump(msg)) rescue => detail puts "Could not dump: #{detail}" return end # Add the hostname to the source @driver.addlog(tmp) rescue => detail puts detail.backtrace if Puppet[:trace] 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. module Puppet::Test class LogCollector def initialize(logs) @logs = logs end def <<(value) @logs << value end end end Puppet::Util::Log.newdesttype :array do match "Puppet::Test::LogCollector" def initialize(messages) @messages = messages end def handle(msg) @messages << msg end end diff --git a/spec/unit/util/log/destinations_spec.rb b/spec/unit/util/log/destinations_spec.rb index 3cb8e24a3..07052b361 100755 --- a/spec/unit/util/log/destinations_spec.rb +++ b/spec/unit/util/log/destinations_spec.rb @@ -1,111 +1,162 @@ #!/usr/bin/env rspec require 'spec_helper' require 'puppet/util/log' describe Puppet::Util::Log.desttypes[:report] do before do @dest = Puppet::Util::Log.desttypes[:report] end it "should require a report at initialization" do @dest.new("foo").report.should == "foo" end it "should send new messages to the report" do report = mock 'report' dest = @dest.new(report) report.expects(:<<).with("my log") dest.handle "my log" end end describe Puppet::Util::Log.desttypes[:file] do before do File.stubs(:open) # prevent actually creating the file @class = Puppet::Util::Log.desttypes[:file] end it "should default to autoflush false" do @class.new('/tmp/log').autoflush.should == false end describe "when matching" do shared_examples_for "file destination" do it "should match an absolute path" do @class.match?(abspath).should be_true end it "should not match a relative path" do @class.match?(relpath).should be_false end end describe "on POSIX systems" do before :each do Puppet.features.stubs(:microsoft_windows?).returns false end let (:abspath) { '/tmp/log' } let (:relpath) { 'log' } it_behaves_like "file destination" end describe "on Windows systems" do before :each do Puppet.features.stubs(:microsoft_windows?).returns true end let (:abspath) { 'C:\\temp\\log.txt' } let (:relpath) { 'log.txt' } it_behaves_like "file destination" end end end describe Puppet::Util::Log.desttypes[:syslog] do let (:klass) { Puppet::Util::Log.desttypes[:syslog] } # these tests can only be run when syslog is present, because # we can't stub the top-level Syslog module describe "when syslog is available", :if => Puppet.features.syslog? do before :each do Syslog.stubs(:opened?).returns(false) Syslog.stubs(:const_get).returns("LOG_KERN").returns(0) Syslog.stubs(:open) end it "should open syslog" do Syslog.expects(:open) klass.new end it "should close syslog" do Syslog.expects(:close) dest = klass.new dest.close end it "should send messages to syslog" do syslog = mock 'syslog' syslog.expects(:info).with("don't panic") Syslog.stubs(:open).returns(syslog) msg = Puppet::Util::Log.new(:level => :info, :message => "don't panic") dest = klass.new dest.handle(msg) end end describe "when syslog is unavailable" do it "should not be a suitable log destination" do Puppet.features.stubs(:syslog?).returns(false) klass.suitable?(:syslog).should be_false end end end +describe Puppet::Util::Log.desttypes[:console] do + describe "when color is available" do + it "should support color output" do + Puppet.stubs(:[]).with(:color).returns(true) + subject.colorize(:red, 'version').should == "\e[0;31mversion\e[0m" + end + + it "should withhold color output when not appropriate" do + Puppet.stubs(:[]).with(:color).returns(false) + subject.colorize(:red, 'version').should == "version" + end + + it "should handle multiple overlapping colors in a stack-like way" do + Puppet.stubs(:[]).with(:color).returns(true) + vstring = subject.colorize(:red, 'version') + subject.colorize(:green, "(#{vstring})").should == "\e[0;32m(\e[0;31mversion\e[0;32m)\e[0m" + end + + it "should handle resets in a stack-like way" do + Puppet.stubs(:[]).with(:color).returns(true) + vstring = subject.colorize(:reset, 'version') + subject.colorize(:green, "(#{vstring})").should == "\e[0;32m(\e[mversion\e[0;32m)\e[0m" + end + end +end + +describe Puppet::Util::Log.desttypes[:telly_prototype_console] do + describe "when color is available" do + it "should support color output" do + Puppet.stubs(:[]).with(:color).returns(true) + subject.colorize(:red, 'version').should == "\e[0;31mversion\e[0m" + end + + it "should withhold color output when not appropriate" do + Puppet.stubs(:[]).with(:color).returns(false) + subject.colorize(:red, 'version').should == "version" + end + + it "should handle multiple overlapping colors in a stack-like way" do + Puppet.stubs(:[]).with(:color).returns(true) + vstring = subject.colorize(:red, 'version') + subject.colorize(:green, "(#{vstring})").should == "\e[0;32m(\e[0;31mversion\e[0;32m)\e[0m" + end + + it "should handle resets in a stack-like way" do + Puppet.stubs(:[]).with(:color).returns(true) + vstring = subject.colorize(:reset, 'version') + subject.colorize(:green, "(#{vstring})").should == "\e[0;32m(\e[mversion\e[0;32m)\e[0m" + end + end +end diff --git a/spec/unit/util/log_spec.rb b/spec/unit/util/log_spec.rb index 68728fe43..e550af195 100755 --- a/spec/unit/util/log_spec.rb +++ b/spec/unit/util/log_spec.rb @@ -1,243 +1,243 @@ #!/usr/bin/env rspec require 'spec_helper' require 'puppet/util/log' describe Puppet::Util::Log do include PuppetSpec::Files it "should write a given message to the specified destination" do arraydest = [] Puppet::Util::Log.newdestination(Puppet::Test::LogCollector.new(arraydest)) Puppet::Util::Log.new(:level => :notice, :message => "foo") message = arraydest.last.message message.should == "foo" 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" + @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 Puppet::Util::Log::DestSyslog do before do @syslog = Puppet::Util::Log::DestSyslog.new end end describe "instances" do before do Puppet::Util::Log.stubs(:newmessage) end [:level, :message, :time, :remote].each do |attr| it "should have a #{attr} attribute" 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.close_all Puppet::Util::Log.expects(:flushqueue) Puppet::Util::Log.newdestination(:console) 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", :'fails_on_ruby_1.9.2' => true 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].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 update Log autoflush when Puppet[:autoflush] is set" do Puppet::Util::Log.expects(:autoflush=).once.with(true) Puppet[:autoflush] = true 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 it "should not create unsuitable log destinations" do Puppet.features.stubs(:syslog?).returns(false) Puppet::Util::Log::DestSyslog.expects(:suitable?) Puppet::Util::Log::DestSyslog.expects(:new).never Puppet::Util::Log.newdestination(:syslog) 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 => make_absolute("/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.source = source log.source.should == "path" end it "should copy over any file and line information" do source = Puppet::Type.type(:file).new :path => make_absolute("/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 describe "to_yaml", :'fails_on_ruby_1.9.2' => true do it "should not include the @version attribute" do log = Puppet::Util::Log.new(:level => "notice", :message => :foo, :version => 100) log.to_yaml_properties.should_not include('@version') end it "should include attributes @level, @message, @source, @tags, and @time" do log = Puppet::Util::Log.new(:level => "notice", :message => :foo, :version => 100) log.to_yaml_properties.should == %w{@level @message @source @tags @time} end it "should include attributes @file and @line if specified" do log = Puppet::Util::Log.new(:level => "notice", :message => :foo, :file => "foo", :line => 35) log.to_yaml_properties.should include('@file') log.to_yaml_properties.should include('@line') end end end