diff --git a/lib/puppet/util/log/destinations.rb b/lib/puppet/util/log/destinations.rb index 4d6c565db..56c82fbdc 100644 --- a/lib/puppet/util/log/destinations.rb +++ b/lib/puppet/util/log/destinations.rb @@ -1,232 +1,234 @@ 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-#{Puppet.run_mode.name}" 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}", $!.backtrace 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" msg.to_s.split("\n").each do |line| @syslog.send(msg.level, line.gsub("%", '%%')) end else msg.to_s.split("\n").each do |line| @syslog.send(msg.level, "(%s) %s" % [msg.source.to_s.gsub("%", ""), line.gsub("%", '%%') ] ) end 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 Puppet::FileSystem.exist?(Puppet::FileSystem.dir(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) # Give ownership to the user and group puppet will run as - begin - FileUtils.chown(Puppet[:user], Puppet[:group], path) unless Puppet::Util::Platform.windows? - rescue ArgumentError, Errno::EPERM - Puppet.err "Unable to set ownership to #{Puppet[:user]}:#{Puppet[:group]} for log file: #{path}" + if Puppet.features.root? && !Puppet::Util::Platform.windows? + begin + FileUtils.chown(Puppet[:user], Puppet[:group], path) + rescue ArgumentError, Errno::EPERM + Puppet.err "Unable to set ownership to #{Puppet[:user]}:#{Puppet[:group]} for log file: #{path}" + end end @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 :logstash_event do require 'time' def format(msg) # logstash_event format is documented at # https://logstash.jira.com/browse/LOGSTASH-675 data = {} data = msg.to_hash data['version'] = 1 data['@timestamp'] = data['time'] data.delete('time') data end def handle(msg) message = format(msg) $stdout.puts message.to_pson end end Puppet::Util::Log.newdesttype :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) levels = { :emerg => { :name => 'Emergency', :color => :hred, :stream => $stderr }, :alert => { :name => 'Alert', :color => :hred, :stream => $stderr }, :crit => { :name => 'Critical', :color => :hred, :stream => $stderr }, :err => { :name => 'Error', :color => :hred, :stream => $stderr }, :warning => { :name => 'Warning', :color => :hred, :stream => $stderr }, :notice => { :name => 'Notice', :color => :reset, :stream => $stdout }, :info => { :name => 'Info', :color => :green, :stream => $stdout }, :debug => { :name => 'Debug', :color => :cyan, :stream => $stdout }, } str = msg.respond_to?(:multiline) ? msg.multiline : msg.to_s str = msg.source == "Puppet" ? str : "#{msg.source}: #{str}" level = levels[msg.level] level[:stream].puts colorize(level[:color], "#{level[:name]}: #{str}") 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 Puppet::Util::Log.newdesttype :eventlog do Puppet::Util::Log::DestEventlog::EVENTLOG_ERROR_TYPE = 0x0001 Puppet::Util::Log::DestEventlog::EVENTLOG_WARNING_TYPE = 0x0002 Puppet::Util::Log::DestEventlog::EVENTLOG_INFORMATION_TYPE = 0x0004 def self.suitable?(obj) Puppet.features.eventlog? end def initialize @eventlog = Win32::EventLog.open("Application") end def to_native(level) case level when :debug,:info,:notice [self.class::EVENTLOG_INFORMATION_TYPE, 0x01] when :warning [self.class::EVENTLOG_WARNING_TYPE, 0x02] when :err,:alert,:emerg,:crit [self.class::EVENTLOG_ERROR_TYPE, 0x03] end end def handle(msg) native_type, native_id = to_native(msg.level) @eventlog.report_event( :source => "Puppet", :event_type => native_type, :event_id => native_id, :data => (msg.source and msg.source != 'Puppet' ? "#{msg.source}: " : '') + msg.to_s ) end def close if @eventlog @eventlog.close @eventlog = nil end end end diff --git a/spec/unit/util/log/destinations_spec.rb b/spec/unit/util/log/destinations_spec.rb index 3c7d6eb68..a9587addd 100755 --- a/spec/unit/util/log/destinations_spec.rb +++ b/spec/unit/util/log/destinations_spec.rb @@ -1,234 +1,242 @@ #! /usr/bin/env ruby require 'spec_helper' require 'json' 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 include PuppetSpec::Files before do File.stubs(:open) # prevent actually creating the file File.stubs(:chown) # prevent chown on non existing file from failing @class = Puppet::Util::Log.desttypes[:file] end it "should default to autoflush false" do @class.new(tmpfile('log')).autoflush.should == true 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", :as_platform => :posix do let (:abspath) { '/tmp/log' } let (:relpath) { 'log' } it_behaves_like "file destination" it "logs an error if it can't chown the file owner & group" do FileUtils.expects(:chown).with(Puppet[:user], Puppet[:group], abspath).raises(Errno::EPERM) + Puppet.features.expects(:root?).returns(true) Puppet.expects(:err).with("Unable to set ownership to #{Puppet[:user]}:#{Puppet[:group]} for log file: #{abspath}") @class.new(abspath) end + + it "doesn't attempt to chown when running as non-root" do + FileUtils.expects(:chown).with(Puppet[:user], Puppet[:group], abspath).never + Puppet.features.expects(:root?).returns(false) + + @class.new(abspath) + end end describe "on Windows systems", :as_platform => :windows do 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[:logstash_event] do describe "when using structured log format with logstash_event schema" do before :each do @msg = Puppet::Util::Log.new(:level => :info, :message => "So long, and thanks for all the fish.", :source => "a dolphin") end it "format should fix the hash to have the correct structure" do dest = described_class.new result = dest.format(@msg) result["version"].should == 1 result["level"].should == :info result["message"].should == "So long, and thanks for all the fish." result["source"].should == "a dolphin" # timestamp should be within 10 seconds Time.parse(result["@timestamp"]).should >= ( Time.now - 10 ) end it "format returns a structure that can be converted to json" do dest = described_class.new hash = dest.format(@msg) JSON.parse(hash.to_json) end it "handle should send the output to stdout" do $stdout.expects(:puts).once dest = described_class.new dest.handle(@msg) end end end describe Puppet::Util::Log.desttypes[:console] do let (:klass) { Puppet::Util::Log.desttypes[:console] } describe "when color is available" do before :each do subject.stubs(:console_has_color?).returns(true) end it "should support color output" do Puppet[:color] = true subject.colorize(:red, 'version').should == "\e[0;31mversion\e[0m" end it "should withhold color output when not appropriate" do Puppet[:color] = false subject.colorize(:red, 'version').should == "version" end it "should handle multiple overlapping colors in a stack-like way" do Puppet[:color] = 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[:color] = true vstring = subject.colorize(:reset, 'version') subject.colorize(:green, "(#{vstring})").should == "\e[0;32m(\e[mversion\e[0;32m)\e[0m" end it "should include the log message's source/context in the output when available" do Puppet[:color] = false $stdout.expects(:puts).with("Info: a hitchhiker: don't panic") msg = Puppet::Util::Log.new(:level => :info, :message => "don't panic", :source => "a hitchhiker") dest = klass.new dest.handle(msg) end end end describe ":eventlog", :if => Puppet::Util::Platform.windows? do before do if Facter.value(:kernelmajversion).to_f < 6.0 pending("requires win32-eventlog gem upgrade to 0.6.2 on Windows 2003") end end let(:klass) { Puppet::Util::Log.desttypes[:eventlog] } def expects_message_with_type(klass, level, eventlog_type, eventlog_id) eventlog = stub('eventlog') eventlog.expects(:report_event).with(has_entries(:source => "Puppet", :event_type => eventlog_type, :event_id => eventlog_id, :data => "a hitchhiker: don't panic")) Win32::EventLog.stubs(:open).returns(eventlog) msg = Puppet::Util::Log.new(:level => level, :message => "don't panic", :source => "a hitchhiker") dest = klass.new dest.handle(msg) end it "supports the eventlog feature" do expect(Puppet.features.eventlog?).to be_true end it "logs to the Application event log" do eventlog = stub('eventlog') Win32::EventLog.expects(:open).with('Application').returns(stub('eventlog')) klass.new end it "logs :debug level as an information type event" do expects_message_with_type(klass, :debug, klass::EVENTLOG_INFORMATION_TYPE, 0x1) end it "logs :warning level as an warning type event" do expects_message_with_type(klass, :warning, klass::EVENTLOG_WARNING_TYPE, 0x2) end it "logs :err level as an error type event" do expects_message_with_type(klass, :err, klass::EVENTLOG_ERROR_TYPE, 0x3) end end