require 'syslog' module Puppet # 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 Log PINK="" GREEN="" YELLOW="" SLATE="" ORANGE="" BLUE="" RESET="" @levels = [:debug,:info,:notice,:warning,:err,:alert,:emerg,:crit] @loglevel = 2 @colors = { :debug => SLATE, :info => GREEN, :notice => PINK, :warning => ORANGE, :err => YELLOW, :alert => BLUE, :emerg => RESET, :crit => RESET } #@destinations = {:syslog => Syslog.open("puppet")} #@destinations = {:console => :console} @destinations = {} # 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) end else @destinations.each { |type, dest| if dest.respond_to?(:flush) dest.flush end if dest.respond_to?(:close) dest.close end } @destinations = {} end Puppet.info "closed" 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::Log.new(hash) else return nil end end def Log.destinations return @destinations.keys 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.include?(dest) return end case dest when "syslog", :syslog if Syslog.opened? Syslog.close end name = Puppet.name name = "puppet-#{name}" unless name =~ /puppet/ @destinations[:syslog] = Syslog.open(name) when /^\// # files Puppet.info "opening %s as a log" % dest # 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(dest)) begin Puppet.recmkdir(File.dirname(dest)) Puppet.info "Creating log directory %s" % File.dirname(dest) rescue => detail Log.newdestination(:console) Puppet.err "Could not create log directory: %s" % detail return end end begin # create the log file, if it doesn't already exist file = File.open(dest,File::WRONLY|File::CREAT|File::APPEND) rescue => detail Log.newdestination(:console) Puppet.err "Could not create log file: %s" % detail return end @destinations[dest] = file when "console", :console @destinations[:console] = :console when Puppet::Server::Logger @destinations[dest] = dest else Puppet.info "Treating %s as a hostname" % dest args = {} if dest =~ /:(\d+)/ args[:Port] = $1 args[:Server] = dest.sub(/:\d+/, '') else args[:Server] = dest end @destinations[dest] = Puppet::Client::LogClient.new(args) 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 # itself. def Log.newmessage(msg) if @levels.index(msg.level) < @loglevel return end @destinations.each { |type, dest| case dest when Module # This is the Syslog module next if msg.remote # 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" dest.send(msg.level, msg.to_s.gsub("%", '%%')) else dest.send(msg.level, "(%s) %s" % [msg.source.to_s.gsub("%", ""), msg.to_s.gsub("%", '%%') ] ) end when File: dest.puts("%s %s (%s): %s" % [msg.time, msg.source, msg.level, msg.to_s]) when :console color = "" reset = "" if Puppet[:color] color = @colors[msg.level] reset = RESET end if msg.source == "Puppet" puts color + "%s: %s" % [ msg.level, msg.to_s ] + reset else puts color + "%s: %s: %s" % [ msg.level, msg.source, msg.to_s ] + reset end when Puppet::Client::LogClient 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 dest.addlog(tmp) #dest.addlog(msg.to_s) sleep(0.5) rescue => detail Puppet.err detail @destinations.delete(type) end end else raise Puppet::Error, "Invalid log destination %s" % dest #puts "Invalid log destination %s" % dest.inspect end } end def Log.sendlevel?(level) @levels.index(level) >= @loglevel end # Reopen all of our logs. def Log.reopen 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 :level, :message, :time, :tags, :remote attr_reader :source def initialize(args) unless args.include?(:level) && args.include?(:message) raise Puppet::DevError, "Puppet::Log called incorrectly" end if args[:level].class == String @level = args[:level].intern elsif args[:level].class == Symbol @level = args[:level] else raise Puppet::DevError, "Level is not a string or symbol: #{args[:level].class}" end # Just return unless we're actually at a level we should send #return unless self.class.sendlevel?(@level) @message = args[:message].to_s @time = Time.now # this should include the host name, and probly lots of other # stuff, at some point unless self.class.validlevel?(level) raise Puppet::DevError, "Invalid message level #{level}" end if args.include?(:tags) @tags = args[:tags] end if args.include?(:source) self.source = args[:source] else @source = "Puppet" end Log.newmessage(self) 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) # We can't store the actual source, we just store the path. This # is a bit of a stupid hack, specifically testing for elements, but # eh. if source.is_a?(Puppet::Element) and source.respond_to?(:path) @source = source.path else @source = source.to_s end unless defined? @tags and @tags if source.respond_to?(:tags) @tags = source.tags end end end def to_s return @message end end end # $Id$