Wie kann ich Ruby Logger Log Ausgabe auf stdout sowie Datei haben?

94

Etwas wie eine Tee-Funktionalität im Logger.

Manish Sapariya
quelle
1
Hinzufügen, | teebevor die Datei für mich funktioniert hat, also Logger.new("| tee test.log"). Beachten Sie das Rohr. Dies war aus einem Tipp auf coderwall.com/p/y_b3ra/…
Mike W
@mjwatts Verwenden Sie diese Option tee --append test.log, um Überschreibungen zu verhindern.
Fangxing

Antworten:

124

Sie können eine Pseudoklasse IOschreiben, die in mehrere IOObjekte schreibt . Etwas wie:

class MultiIO
  def initialize(*targets)
     @targets = targets
  end

  def write(*args)
    @targets.each {|t| t.write(*args)}
  end

  def close
    @targets.each(&:close)
  end
end

Dann legen Sie das als Ihre Protokolldatei fest:

log_file = File.open("log/debug.log", "a")
Logger.new MultiIO.new(STDOUT, log_file)

Jedesmal , wenn LoggerAnrufe putsauf Ihrem MultiIOObjekt, wird es sowohl schreiben STDOUTund Ihre Log - Datei.

Bearbeiten: Ich ging voran und fand den Rest der Oberfläche heraus. Ein Protokollgerät muss auf writeund close(nicht puts) antworten . Solange MultiIOauf diese reagiert und sie auf die realen E / A-Objekte übertragen werden, sollte dies funktionieren.

David
quelle
Wenn Sie sich den ctor von logger ansehen, werden Sie feststellen, dass dies die Protokollrotation durcheinander bringt. def initialize(log = nil, opt = {}) @dev = @filename = @shift_age = @shift_size = nil @mutex = LogDeviceMutex.new if log.respond_to?(:write) and log.respond_to?(:close) @dev = log else @dev = open_logfile(log) @dev.sync = true @filename = log @shift_age = opt[:shift_age] || 7 @shift_size = opt[:shift_size] || 1048576 end end
JeffCharter
3
Hinweis in Ruby 2.2 @targets.each(&:close)wird abgeschrieben.
xis
Arbeitete für mich, bis mir klar wurde, dass ich regelmäßig aufrufen musste: close on log_file, um log_file zu erhalten, um zu aktualisieren, was der Logger protokolliert hatte (im Wesentlichen ein "Speichern"). STDOUT gefiel es nicht: nahe dran zu sein, die MultoIO-Idee zu besiegen. Ein Hack zum Überspringen wurde hinzugefügt: Schließen mit Ausnahme der Klasse Datei, aber ich wünschte, ich hätte eine elegantere Lösung.
Kim Miller
48

@ Davids Lösung ist sehr gut. Ich habe eine generische Delegatorklasse für mehrere Ziele basierend auf seinem Code erstellt.

require 'logger'

class MultiDelegator
  def initialize(*targets)
    @targets = targets
  end

  def self.delegate(*methods)
    methods.each do |m|
      define_method(m) do |*args|
        @targets.map { |t| t.send(m, *args) }
      end
    end
    self
  end

  class <<self
    alias to new
  end
end

log_file = File.open("debug.log", "a")
log = Logger.new MultiDelegator.delegate(:write, :close).to(STDOUT, log_file)
jonas054
quelle
Könnten Sie bitte erklären, wie dies besser ist oder was verbesserte Dienstprogramme dieses Ansatzes sind als das einfache, das von David
Manish Sapariya am
5
Es ist die Trennung von Bedenken. MultiDelegator kann nur Anrufe an mehrere Ziele delegieren. Die Tatsache, dass ein Protokollierungsgerät eine Schreib- und eine Abschlussmethode benötigt, ist im Aufrufer implementiert. Dies macht MultiDelegator in anderen Situationen als der Protokollierung verwendbar.
Jonas054
Schöne Lösung. Ich habe versucht, damit die Ausgabe meiner Rake-Tasks in eine Protokolldatei zu übertragen. Um es mit Puts zum Laufen zu bringen (um $ stdout.puts aufrufen zu können, ohne "private Methode" Puts "aufgerufen zu bekommen), musste ich einige weitere Methoden hinzufügen: log_file = File.open (" tmp / rake.log "," a ") $ stdout = MultiDelegator.delegate (: write ,: close ,: put ,: print) .to (STDOUT, log_file) Wäre schön, wenn es möglich wäre, eine Tee-Klasse zu erstellen, von der geerbt wurde MultiDelegator, wie Sie es mit der Delegator-Klasse in stdlib tun können ...
Tyler Rick
Ich habe mir eine Delegator-ähnliche Implementierung ausgedacht, die ich DelegatorToAll genannt habe. Auf diese Weise müssen Sie nicht alle Methoden auflisten, die Sie delegieren möchten, da alle Methoden delegiert werden, die in der Delegate-Klasse (IO) definiert sind: class Tee <DelegateToAllClass (IO) end $ stdout = Tee.new (STDOUT , File.open ("# { FILE } .log", "a")) Weitere Informationen finden Sie unter gist.github.com/TylerRick/4990898 .
Tyler Rick
1
Ihre Lösung gefällt mir sehr gut, aber sie ist nicht gut als generischer Delegator, der mehrfach verwendet werden kann, da jede Delegation alle Instanzen mit neuen Methoden verschmutzt. Ich habe unten eine Antwort gepostet ( stackoverflow.com/a/36659911/123376 ), die dieses Problem behebt. Ich habe eher eine Antwort als eine Bearbeitung gepostet, da es möglicherweise lehrreich ist, den Unterschied zwischen den beiden Implementierungen zu erkennen, da ich auch Beispiele gepostet habe.
Rado
35

Wenn Sie sich in Rails 3 oder 4 befinden, verfügt Rails 4, wie in diesem Blogbeitrag hervorgehoben , über diese integrierte Funktionalität . So können Sie tun:

# config/environment/production.rb
file_logger = Logger.new(Rails.root.join("log/alternative-output.log"))
config.logger.extend(ActiveSupport::Logger.broadcast(file_logger))

Oder wenn Sie auf Rails 3 sind, können Sie es zurückportieren:

# config/initializers/alternative_output_log.rb

# backported from rails4
module ActiveSupport
  class Logger < ::Logger
    # Broadcasts logs to multiple loggers. Returns a module to be
    # `extended`'ed into other logger instances.
    def self.broadcast(logger)
      Module.new do
        define_method(:add) do |*args, &block|
          logger.add(*args, &block)
          super(*args, &block)
        end

        define_method(:<<) do |x|
          logger << x
          super(x)
        end

        define_method(:close) do
          logger.close
          super()
        end

        define_method(:progname=) do |name|
          logger.progname = name
          super(name)
        end

        define_method(:formatter=) do |formatter|
          logger.formatter = formatter
          super(formatter)
        end

        define_method(:level=) do |level|
          logger.level = level
          super(level)
        end
      end
    end
  end
end

file_logger = Logger.new(Rails.root.join("log/alternative-output.log"))
Rails.logger.extend(ActiveSupport::Logger.broadcast(file_logger))
Phillbaker
quelle
Gilt dies außerhalb von Schienen oder nur für Schienen?
Ed Sykes
Es basiert auf ActiveSupport. Wenn Sie diese Abhängigkeit bereits haben, können Sie extendjede ActiveSupport::LoggerInstanz wie oben gezeigt ausführen .
Phillipsbaker
Danke, es war hilfreich.
Lucas
Ich denke, dies ist die einfachste und effektivste Antwort, obwohl ich bei der Verwendung der config.logger.extend()Konfiguration in meiner Umgebung etwas seltsam war . Stattdessen setze ich config.loggerauf STDOUTin meinem Umfeld, erweitert dann den Logger in verschiedenen initializers.
Mattsch
14

Für diejenigen, die es einfach mögen:

log = Logger.new("| tee test.log") # note the pipe ( '|' )
log.info "hi" # will log to both STDOUT and test.log

Quelle

Oder drucken Sie die Nachricht im Logger-Formatierer:

log = Logger.new("test.log")
log.formatter = proc do |severity, datetime, progname, msg|
    puts msg
    msg
end
log.info "hi" # will log to both STDOUT and test.log

Ich verwende diese Technik tatsächlich, um in eine Protokolldatei, einen Cloud-Logger-Dienst (Logentries) und in einer Entwicklungsumgebung zu drucken - auch in STDOUT zu drucken.

Igor
quelle
2
"| tee test.log"wird die alten Ausgaben überschreiben, kann "| tee -a test.log"stattdessen sein
fangxing
13

Obwohl ich die anderen Vorschläge sehr mag, stellte ich fest, dass ich das gleiche Problem hatte, aber die Möglichkeit haben wollte, unterschiedliche Protokollierungsstufen für STDERR und die Datei zu haben.

Am Ende hatte ich eine Routing-Strategie, die auf Logger-Ebene und nicht auf E / A-Ebene multiplext, sodass jeder Logger dann auf unabhängigen Log-Ebenen arbeiten kann:

class MultiLogger
  def initialize(*targets)
    @targets = targets
  end

  %w(log debug info warn error fatal unknown).each do |m|
    define_method(m) do |*args|
      @targets.map { |t| t.send(m, *args) }
    end
  end
end

stderr_log = Logger.new(STDERR)
file_log = Logger.new(File.open('logger.log', 'a'))

stderr_log.level = Logger::INFO
file_log.level = Logger::DEBUG

log = MultiLogger.new(stderr_log, file_log)
dsz
quelle
1
Diese Lösung gefällt mir am besten, da sie (1) einfach ist und (2) Sie dazu ermutigt, Ihre Logger-Klassen wiederzuverwenden, anstatt davon auszugehen, dass alles in eine Datei geht. In meinem Fall möchte ich mich bei STDOUT und einem GELF-Appender für Graylog anmelden. Ein MultiLoggerähnliches wie @dsz zu haben, passt sehr gut. Danke für das Teilen!
Eric Kramer
Abschnitt hinzugefügt, um Pseudovariablen (Setter / Getter) zu behandeln
Eric Kramer
11

Sie können dem Logger auch mehrere Geräteprotokollierungsfunktionen direkt hinzufügen:

require 'logger'

class Logger
  # Creates or opens a secondary log file.
  def attach(name)
    @logdev.attach(name)
  end

  # Closes a secondary log file.
  def detach(name)
    @logdev.detach(name)
  end

  class LogDevice # :nodoc:
    attr_reader :devs

    def attach(log)
      @devs ||= {}
      @devs[log] = open_logfile(log)
    end

    def detach(log)
      @devs ||= {}
      @devs[log].close
      @devs.delete(log)
    end

    alias_method :old_write, :write
    def write(message)
      old_write(message)

      @devs ||= {}
      @devs.each do |log, dev|
        dev.write(message)
      end
    end
  end
end

Zum Beispiel:

logger = Logger.new(STDOUT)
logger.warn('This message goes to stdout')

logger.attach('logfile.txt')
logger.warn('This message goes both to stdout and logfile.txt')

logger.detach('logfile.txt')
logger.warn('This message goes just to stdout')
Ramon de C Valle
quelle
9

Hier ist eine weitere Implementierung, die von der Antwort von @ jonas054 inspiriert wurde .

Dies verwendet ein ähnliches Muster wie Delegator. Auf diese Weise müssen Sie nicht alle Methoden auflisten, die Sie delegieren möchten, da alle Methoden delegiert werden, die in einem der Zielobjekte definiert sind:

class Tee < DelegateToAllClass(IO)
end

$stdout = Tee.new(STDOUT, File.open("#{__FILE__}.log", "a"))

Sie sollten dies auch mit Logger verwenden können.

delegate_to_all.rb ist hier verfügbar: https://gist.github.com/TylerRick/4990898

Tyler Rick
quelle
3

Die Antwort von @ jonas054 oben ist großartig, aber sie verschmutzt die MultiDelegatorKlasse mit jedem neuen Delegierten. Wenn Sie MultiDelegatormehrmals verwenden, werden der Klasse weiterhin Methoden hinzugefügt, was unerwünscht ist. (Siehe unten zum Beispiel)

Hier ist dieselbe Implementierung, jedoch mit anonymen Klassen, damit die Methoden die Delegatorklasse nicht verschmutzen.

class BetterMultiDelegator

  def self.delegate(*methods)
    Class.new do
      def initialize(*targets)
        @targets = targets
      end

      methods.each do |m|
        define_method(m) do |*args|
          @targets.map { |t| t.send(m, *args) }
        end
      end

      class <<self
        alias to new
      end
    end # new class
  end # delegate

end

Hier ist ein Beispiel für die Methodenverschmutzung mit der ursprünglichen Implementierung im Gegensatz zur modifizierten Implementierung:

tee = MultiDelegator.delegate(:write).to(STDOUT)
tee.respond_to? :write
# => true
tee.respond_to? :size
# => false 

Alles ist gut oben. teehat eine writeMethode, aber keine sizeMethode wie erwartet. Überlegen Sie nun, wann wir einen weiteren Delegaten erstellen:

tee2 = MultiDelegator.delegate(:size).to("bar")
tee2.respond_to? :size
# => true
tee2.respond_to? :write
# => true   !!!!! Bad
tee.respond_to? :size
# => true   !!!!! Bad

Oh nein, tee2reagiert sizewie erwartet, aber es reagiert auch writewegen des ersten Delegierten. Schon teejetzt reagiert sizeaufgrund der Methode Verschmutzung.

Vergleichen Sie dies mit der anonymen Klassenlösung, alles ist wie erwartet:

see = BetterMultiDelegator.delegate(:write).to(STDOUT)
see.respond_to? :write
# => true
see.respond_to? :size
# => false

see2 = BetterMultiDelegator.delegate(:size).to("bar")
see2.respond_to? :size
# => true
see2.respond_to? :write
# => false
see.respond_to? :size
# => false
Rado
quelle
2

Sind Sie auf den Standardlogger beschränkt?

Wenn nicht, können Sie log4r verwenden :

require 'log4r' 

LOGGER = Log4r::Logger.new('mylog')
LOGGER.outputters << Log4r::StdoutOutputter.new('stdout')
LOGGER.outputters << Log4r::FileOutputter.new('file', :filename => 'test.log') #attach to existing log-file

LOGGER.info('aa') #Writs on STDOUT and sends to file

Ein Vorteil: Sie können auch verschiedene Protokollebenen für stdout und file definieren.

knut
quelle
1

Ich bin zu der gleichen Idee übergegangen, "alle Methoden an Unterelemente zu delegieren", die andere Leute bereits untersucht haben, aber für jeden von ihnen den Rückgabewert des letzten Aufrufs der Methode zurückgeben. Wenn ich es nicht tat, brach es, logger-colorswas eine erwartete Integerund die Karte eine zurückgab Array.

class MultiIO
  def self.delegate_all
    IO.methods.each do |m|
      define_method(m) do |*args|
        ret = nil
        @targets.each { |t| ret = t.send(m, *args) }
        ret
      end
    end
  end

  def initialize(*targets)
    @targets = targets
    MultiIO.delegate_all
  end
end

Dadurch wird jede Methode auf alle Ziele neu verknüpft und nur der Rückgabewert des letzten Aufrufs zurückgegeben.

Wenn Sie Farben möchten, müssen STDOUT oder STDERR als letzte gesetzt werden, da dies die einzigen beiden Farben sind, in denen Farben ausgegeben werden sollen. Dann werden aber auch Farben in Ihre Datei ausgegeben.

logger = Logger.new MultiIO.new(File.open("log/test.log", 'w'), STDOUT)
logger.error "Roses are red"
logger.unknown "Violets are blue"
Jerska
quelle
1

Ich habe ein kleines RubyGem geschrieben, mit dem Sie verschiedene Dinge tun können:

# Pipe calls to an instance of Ruby's logger class to $stdout
require 'teerb'

log_file = File.open("debug.log", "a")
logger = Logger.new(TeeRb::IODelegate.new(log_file, STDOUT))

logger.warn "warn"
$stderr.puts "stderr hello"
puts "stdout hello"

Sie finden den Code auf github: teerb

Patrick Hüsler
quelle
1

Noch ein Weg. Wenn Sie die Tagged-Protokollierung verwenden und Tags auch in einer anderen Protokolldatei benötigen, können Sie dies auf diese Weise tun

# backported from rails4
# config/initializers/active_support_logger.rb
module ActiveSupport
 class Logger < ::Logger

 # Broadcasts logs to multiple loggers. Returns a module to be
 # `extended`'ed into other logger instances.
 def self.broadcast(logger)
  Module.new do
    define_method(:add) do |*args, &block|
      logger.add(*args, &block)
      super(*args, &block)
    end

    define_method(:<<) do |x|
      logger << x
      super(x)
    end

    define_method(:close) do
      logger.close
      super()
    end

    define_method(:progname=) do |name|
      logger.progname = name
      super(name)
    end

    define_method(:formatter=) do |formatter|
      logger.formatter = formatter
      super(formatter)
    end

    define_method(:level=) do |level|
      logger.level = level
      super(level)
    end

   end # Module.new
 end # broadcast

 def initialize(*args)
   super
   @formatter = SimpleFormatter.new
 end

  # Simple formatter which only displays the message.
  class SimpleFormatter < ::Logger::Formatter
   # This method is invoked when a log event occurs
   def call(severity, time, progname, msg)
   element = caller[4] ? caller[4].split("/").last : "UNDEFINED"
    "#{Thread.current[:activesupport_tagged_logging_tags]||nil } # {time.to_s(:db)} #{severity} #{element} -- #{String === msg ? msg : msg.inspect}\n"
   end
  end

 end # class Logger
end # module ActiveSupport

custom_logger = ActiveSupport::Logger.new(Rails.root.join("log/alternative_#{Rails.env}.log"))
Rails.logger.extend(ActiveSupport::Logger.broadcast(custom_logger))

Danach erhalten Sie UUID-Tags im alternativen Logger

["fbfea87d1d8cc101a4ff9d12461ae810"] 2015-03-12 16:54:04 INFO logger.rb:28:in `call_app' -- 
["fbfea87d1d8cc101a4ff9d12461ae810"] 2015-03-12 16:54:04 INFO   logger.rb:31:in `call_app' -- Started POST "/psp/entrypoint" for 192.168.56.1 at 2015-03-12 16:54:04 +0700

Hoffe das hilft jemandem.

retgoat
quelle
Einfach, zuverlässig und funktioniert hervorragend. Vielen Dank! Beachten Sie, dass ActiveSupport::Loggermit diesem der Box funktioniert - Sie müssen nur verwenden Rails.logger.extendmit ActiveSupport::Logger.broadcast(...).
XtraSimplicity
0

Noch eine Option ;-)

require 'logger'

class MultiDelegator
  def initialize(*targets)
    @targets = targets
  end

  def method_missing(method_sym, *arguments, &block)
    @targets.each do |target|
      target.send(method_sym, *arguments, &block) if target.respond_to?(method_sym)
    end
  end
end

log = MultiDelegator.new(Logger.new(STDOUT), Logger.new(File.open("debug.log", "a")))

log.info('Hello ...')
Michael Voigt
quelle
0

Ich mag den MultiIO- Ansatz. Es funktioniert gut mit Ruby Logger . Wenn Sie reines E / A verwenden , funktioniert es nicht mehr, da einige Methoden fehlen, über die E / A-Objekte erwartet werden. Pipes wurden hier bereits erwähnt: Wie kann ich eine Ruby-Logger-Protokollausgabe sowohl an stdout als auch an eine Datei senden? . Hier ist, was für mich am besten funktioniert.

def watch(cmd)
  output = StringIO.new
  IO.popen(cmd) do |fd|
    until fd.eof?
      bit = fd.getc
      output << bit
      $stdout.putc bit
    end
  end
  output.rewind
  [output.read, $?.success?]
ensure
  output.close
end

result, success = watch('./my/shell_command as a String')

Hinweis Ich weiß, dass dies die Frage nicht direkt beantwortet, aber stark verwandt ist. Immer wenn ich nach einer Ausgabe für mehrere E / A gesucht habe, bin ich auf diesen Thread gestoßen. Ich hoffe, Sie finden dies auch nützlich.

Knugie
quelle
0

Dies ist eine Vereinfachung der @ rado-Lösung.

def delegator(*methods)
  Class.new do
    def initialize(*targets)
      @targets = targets
    end

    methods.each do |m|
      define_method(m) do |*args|
        @targets.map { |t| t.send(m, *args) }
      end
    end

    class << self
      alias for new
    end
  end # new class
end # delegate

Es hat alle die gleichen Vorteile wie sein, ohne dass der Wrapper der äußeren Klasse benötigt wird. Es ist ein nützliches Dienstprogramm in einer separaten Ruby-Datei.

Verwenden Sie es als Einzeiler, um Delegatorinstanzen wie folgt zu generieren:

IO_delegator_instance = delegator(:write, :read).for(STDOUT, STDERR)
IO_delegator_instance.write("blah")

ODER verwenden Sie es als Fabrik wie folgt:

logger_delegator_class = delegator(:log, :warn, :error)
secret_delegator = logger_delegator_class(main_logger, secret_logger)
secret_delegator.warn("secret")

general_delegator = logger_delegator_class(main_logger, debug_logger, other_logger) 
general_delegator.log("message")
Charles Murphy
quelle
0

Sie können Loog::TeeObjekt aus loogEdelstein verwenden:

require 'loog'
logger = Loog::Tee.new(first, second)

Genau das, wonach Sie suchen.

yegor256
quelle
0

Wenn Sie mit der Verwendung einverstanden sind ActiveSupport, würde ich das Auschecken sehr empfehlen. Dies ActiveSupport::Logger.broadcastist eine hervorragende und sehr präzise Möglichkeit, einem Protokollierer zusätzliche Protokollziele hinzuzufügen.

In der Tat, wenn Sie Rails verwenden 4+ (Stand dieser begehen ), brauchen Sie nicht zu tun , etwas , das gewünschte Verhalten zu bekommen - zumindest wenn Sie mit dem rails console. Wenn Sie das verwenden rails console, wird Rails automatisch so erweitert Rails.logger, dass es sowohl an das übliche Dateiziel ( log/production.logz. B.) als auch Folgendes ausgibt STDERR:

    console do |app|
      
      unless ActiveSupport::Logger.logger_outputs_to?(Rails.logger, STDERR, STDOUT)
        console = ActiveSupport::Logger.new(STDERR)
        Rails.logger.extend ActiveSupport::Logger.broadcast console
      end
      ActiveRecord::Base.verbose_query_logs = false
    end

Aus unbekannten und unglücklichen Gründen ist diese Methode nicht dokumentiert. Sie können sich jedoch auf den Quellcode oder die Blog-Beiträge beziehen, um zu erfahren, wie sie funktioniert, oder Beispiele anzeigen.

https://www.joshmcarthur.com/til/2018/08/16/logging-to-multiple-destinations-using-activesupport-4.html hat ein weiteres Beispiel:

require "active_support/logger"
console_logger = ActiveSupport::Logger.new(STDOUT)
file_logger = ActiveSupport::Logger.new("my_log.log")
combined_logger = console_logger.extend(ActiveSupport::Logger.broadcast(file_logger))

combined_logger.debug "Debug level"
Tyler Rick
quelle
0

Ich habe dieses Bedürfnis auch kürzlich, also habe ich eine Bibliothek implementiert, die dies tut. Ich habe gerade diese StackOverflow-Frage entdeckt und stelle sie daher allen zur Verfügung, die sie benötigen: https://github.com/agis/multi_io .

Im Vergleich zu den anderen hier genannten Lösungen ist dies ein IOeigenständiges Objekt, sodass es als Ersatz für andere reguläre E / A-Objekte (Dateien, Sockets usw.) verwendet werden kann.

Das heißt, ich habe noch nicht alle Standard-E / A-Methoden implementiert, aber diejenigen, die der E / A-Semantik folgen (z. B. #writegibt die Summe der Anzahl der Bytes zurück, die in alle zugrunde liegenden E / A-Ziele geschrieben wurden).

Agis
quelle
-3

Ich denke, Ihr STDOUT wird für kritische Laufzeitinformationen und Fehler verwendet.

Also benutze ich

  $log = Logger.new('process.log', 'daily')

Debug und regelmäßige Protokollierung zu protokollieren, und schrieb dann ein paar

  puts "doing stuff..."

wo ich STDOUT-Informationen sehen muss, dass meine Skripte überhaupt ausgeführt wurden!

Bah, nur meine 10 Cent :-)

Rupweb
quelle