Comment puis-je avoir la sortie du journal de ruby ​​logger vers stdout ainsi qu'un fichier?


94

Quelque chose comme une fonctionnalité de tee dans Logger.


1
L'ajout | teeavant le fichier a fonctionné pour moi, donc Logger.new("| tee test.log"). Notez le tuyau. Cela provenait
Mike W

@mjwatts Utilisé tee --append test.logpour empêcher les écrasements.
fangxing

Réponses:


124

Vous pouvez écrire une pseudo IOclasse qui écrira sur plusieurs IOobjets. Quelque chose comme:

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

Ensuite, définissez cela comme votre fichier journal:

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

Chaque fois que vous Loggerappelez putsvotre MultiIOobjet, il écrira à la fois dans STDOUTvotre fichier journal.

Edit: je suis allé de l'avant et j'ai compris le reste de l'interface. Un périphérique de journal doit répondre à writeet close(pas puts). Tant que MultiIOrépond à ceux-ci et les envoie par proxy aux objets d'E / S réels, cela devrait fonctionner.


si vous regardez le cteur de l'enregistreur, vous verrez que cela gâchera la rotation du journal. 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 le

3
Remarque dans Ruby 2.2, @targets.each(&:close)est amorti.
xis

A travaillé pour moi jusqu'à ce que je réalise que je devais appeler périodiquement: close sur log_file pour obtenir log_file pour mettre à jour ce que l'enregistreur avait enregistré (essentiellement une "sauvegarde"). STDOUT n'aimait pas: fermer être appelé dessus, en quelque sorte vaincre l'idée de MultoIO. Ajout d'un hack à ignorer: fermer sauf pour la classe File, mais j'aurais aimé avoir une solution plus élégante.
Kim Miller

48

La solution de @ David est très bonne. J'ai créé une classe de délégation générique pour plusieurs cibles en fonction de son code.

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)

Pourriez-vous s'il vous plaît expliquer, en quoi c'est mieux ou quels sont les utilitaires améliorés de cette approche que celle suggérée par David
Manish Sapariya

5
C'est la séparation des préoccupations. MultiDelegator ne sait que déléguer des appels à plusieurs cibles. Le fait qu'un périphérique de journalisation nécessite une méthode d'écriture et de fermeture est implémenté dans l'appelant. Cela rend MultiDelegator utilisable dans d'autres situations que la journalisation.
jonas054

Belle solution. J'ai essayé de l'utiliser pour transférer la sortie de mes tâches de rake dans un fichier journal. Afin de le faire fonctionner avec les put (pour pouvoir appeler $ stdout.puts sans obtenir la "méthode privée` met 'appelée "), j'ai dû ajouter quelques méthodes supplémentaires: log_file = File.open (" tmp / rake.log "," a ") $ stdout = MultiDelegator.delegate (: write,: close,: met,: print) .to (STDOUT, log_file) Ce serait bien s'il était possible de créer une classe Tee héritée de MultiDelegator, comme vous pouvez le faire avec la classe Delegator dans stdlib ...
Tyler Rick

Je suis venu avec une implémentation de type Delegator de ce que j'ai appelé DelegatorToAll. De cette façon, vous n'avez pas à lister toutes les méthodes que vous souhaitez déléguer, car il déléguera toutes les méthodes qui sont définies dans la classe déléguée (IO): class Tee <DelegateToAllClass (IO) end $ stdout = Tee.new (STDOUT , File.open ("# { FILE } .log", "a")) Voir gist.github.com/TylerRick/4990898 pour plus de détails.
Tyler Rick

1
J'aime beaucoup votre solution, mais ce n'est pas bon en tant que délégant générique qui peut être utilisé plusieurs fois car chaque délégation pollue toutes les instances avec de nouvelles méthodes. J'ai posté une réponse ci-dessous ( stackoverflow.com/a/36659911/123376 ) qui résout ce problème. J'ai posté une réponse plutôt qu'une modification car il peut être éducatif de voir la différence entre les deux implémentations car j'ai également publié des exemples.
Rado

35

Si vous êtes dans Rails 3 ou 4, comme le souligne cet article de blog , Rails 4 intègre cette fonctionnalité . Vous pouvez donc faire:

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

Ou si vous êtes sur Rails 3, vous pouvez le rétroporter:

# 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))

cela s'applique-t-il à l'extérieur des rails ou uniquement des rails?
Ed Sykes

Il est basé sur ActiveSupport, donc si vous avez déjà cette dépendance, vous pouvez extendn'importe quelle ActiveSupport::Loggerinstance comme indiqué ci-dessus.
phillbaker

Merci, c'était utile.
Lucas

Je pense que c'est la réponse la plus simple et la plus efficace, même si j'ai eu une certaine bizarrerie en utilisant la config.logger.extend()configuration de mon environnement. Au lieu de cela, je me mis config.loggerà STDOUTdans mon environnement, puis étendu l'enregistreur dans différentes initializers.
mattsch

14

Pour ceux qui aiment la simplicité:

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

la source

Ou imprimez le message dans le formateur Logger:

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

J'utilise en fait cette technique pour imprimer dans un fichier journal, un service d'enregistrement dans le cloud (entrées de journaux) et s'il s'agit d'un environnement de développement, j'imprime également sur STDOUT.


2
"| tee test.log"remplacera les anciennes sorties, peut-être à la "| tee -a test.log"place
fangxing

13

Bien que j'aime assez les autres suggestions, j'ai trouvé que j'avais le même problème, mais je voulais pouvoir avoir des niveaux de journalisation différents pour STDERR et le fichier.

Je me suis retrouvé avec une stratégie de routage qui se multiplexe au niveau de l'enregistreur plutôt qu'au niveau des E / S, afin que chaque enregistreur puisse alors fonctionner à des niveaux de journalisation indépendants:

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)

1
J'aime mieux cette solution car elle est (1) simple et (2) vous encourage à réutiliser vos classes Logger au lieu de supposer que tout va dans un fichier. Dans mon cas, j'aimerais me connecter à STDOUT et à un appender GELF pour Graylog. Avoir un MultiLoggercomme @dsz décrit est un excellent choix. Merci d'avoir partagé!
Eric Kramer

Ajout d'une section pour gérer les pseudovariables (setters / getters)
Eric Kramer

11

Vous pouvez également ajouter la fonctionnalité de journalisation de plusieurs appareils directement dans l'enregistreur:

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

Par exemple:

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')

9

Voici une autre implémentation, inspirée de la réponse de @ jonas054 .

Cela utilise un modèle similaire à Delegator. De cette façon, vous n'avez pas à répertorier toutes les méthodes que vous souhaitez déléguer, car cela déléguera toutes les méthodes définies dans l'un des objets cibles:

class Tee < DelegateToAllClass(IO)
end

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

Vous devriez également pouvoir l'utiliser avec Logger.

Delegate_to_all.rb est disponible ici: https://gist.github.com/TylerRick/4990898



3

La réponse de @ jonas054 ci-dessus est excellente, mais elle pollue la MultiDelegatorclasse à chaque nouveau délégué. Si vous utilisez MultiDelegatorplusieurs fois, il continuera d'ajouter des méthodes à la classe, ce qui n'est pas souhaitable. (Voir ci-dessous par exemple)

Voici la même implémentation, mais en utilisant des classes anonymes pour que les méthodes ne polluent pas la classe de délégation.

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

Voici un exemple de la méthode pollution avec l'implémentation d'origine, par opposition à l'implémentation modifiée:

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

Tout va bien dessus. teea une writeméthode, mais aucune sizeméthode comme prévu. Maintenant, considérez quand nous créons un autre délégué:

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

Oh non, tee2répond sizecomme prévu, mais il répond aussi à writecause du premier délégué. Même teemaintenant répond à sizecause de la pollution de la méthode.

Comparez cela à la solution de classe anonyme, tout est comme prévu:

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

2

Êtes-vous limité à l'enregistreur standard?

Sinon, vous pouvez utiliser log4r :

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

Un avantage: vous pouvez également définir différents niveaux de journalisation pour stdout et file.


1

Je suis allé à la même idée de "déléguer toutes les méthodes à des sous-éléments" que d'autres personnes ont déjà exploré, mais je retourne pour chacun d'eux la valeur de retour du dernier appel de la méthode. Si je ne le faisais pas, il se cassait logger-colorset attendait un Integeret la carte retournait un 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

Cela redéléguera chaque méthode à toutes les cibles et ne retournera que la valeur de retour du dernier appel.

De plus, si vous voulez des couleurs, STDOUT ou STDERR doivent être mis en dernier, car ce sont les deux seuls où les couleurs sont censées être sorties. Mais alors, il produira également des couleurs dans votre fichier.

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

1

J'ai écrit un petit RubyGem qui vous permet de faire plusieurs de ces choses:

# 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"

Vous pouvez trouver le code sur github: teerb


1

Encore une façon. Si vous utilisez la journalisation balisée et que vous avez également besoin de balises dans un autre fichier journal, vous pouvez le faire de cette manière

# 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))

Après cela, vous obtiendrez des balises uuid dans un enregistreur alternatif

["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

J'espère que cela aide quelqu'un.


Simple, fiable et fonctionne avec brio. Merci! Notez que cela ActiveSupport::Loggerfonctionne hors de la boîte avec cela - il vous suffit de l'utiliser Rails.logger.extendavec ActiveSupport::Logger.broadcast(...).
XtraSimplicity

0

Encore une 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 ...')

0

J'aime l' approche MultiIO . Cela fonctionne bien avec Ruby Logger . Si vous utilisez pure IO, il cesse de fonctionner car il manque certaines méthodes que les objets IO devraient avoir. Les tuyaux ont déjà été mentionnés ici: Comment puis-je avoir la sortie du journal de ruby ​​logger sur stdout ainsi que sur un fichier? . Voici ce qui fonctionne le mieux pour moi.

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')

Notez que je sais que cela ne répond pas directement à la question, mais c'est fortement lié. Chaque fois que je cherchais une sortie vers plusieurs E / S, je suis tombé sur ce fil, j'espère donc que vous le trouverez également utile.


0

Il s'agit d'une simplification de la solution de @ rado.

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

Il a tous les mêmes avantages que le sien sans avoir besoin de l'emballage de classe externe. C'est un utilitaire utile à avoir dans un fichier ruby ​​séparé.

Utilisez-le comme une ligne unique pour générer des instances de délégant comme ceci:

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

OU utilisez-le comme une usine comme ceci:

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")

0

Vous pouvez utiliser un Loog::Teeobjet de looggem:

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

Exactement ce que vous recherchez.


0

Si vous êtes d'accord avec l'utilisation ActiveSupport, je vous recommande vivement de vérifier ActiveSupport::Logger.broadcast, ce qui est un moyen excellent et très concis d'ajouter des destinations de journal supplémentaires à un enregistreur.

En fait, si vous utilisez Rails 4+ ( à partir de ce commit ), vous n'avez pas besoin de faire quoi que ce soit pour obtenir le comportement souhaité - du moins si vous utilisez le rails console. Chaque fois que vous utilisez le rails console, Rails s'étend automatiquement deRails.logger telle sorte qu'il sort à la fois vers sa destination de fichier habituelle ( log/production.logpar exemple) et 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

Pour une raison inconnue et malheureuse, cette méthode n'est pas documentée, mais vous pouvez vous référer au code source ou aux articles de blog pour savoir comment cela fonctionne ou voir des exemples.

https://www.joshmcarthur.com/til/2018/08/16/logging-to-multiple-destinations-using-activesupport-4.html a un autre exemple:

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"

0

J'ai également ce besoin récemment, j'ai donc implémenté une bibliothèque qui fait cela. Je viens de découvrir cette question StackOverflow, donc je la mets là-bas pour tous ceux qui en ont besoin: https://github.com/agis/multi_io .

Par rapport aux autres solutions mentionnées ici, cela s'efforce d'être un IOobjet à part entière, de sorte qu'il peut être utilisé comme un remplacement instantané pour d'autres objets IO réguliers (fichiers, sockets, etc.)

Cela dit, je n'ai pas encore implémenté toutes les méthodes IO standard, mais celles qui le sont suivent la sémantique IO (par exemple, #writeretourne la somme du nombre d'octets écrits sur toutes les cibles IO sous-jacentes).


-3

Je pense que votre STDOUT est utilisé pour les informations d'exécution critiques et les erreurs soulevées.

Alors j'utilise

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

pour enregistrer le débogage et la journalisation régulière, puis a écrit quelques

  puts "doing stuff..."

où j'ai besoin de voir les informations STDOUT que mes scripts étaient en cours d'exécution!

Bah, juste mes 10 centimes :-)

En utilisant notre site, vous reconnaissez avoir lu et compris notre politique liée aux cookies et notre politique de confidentialité.
Licensed under cc by-sa 3.0 with attribution required.