web-dev-qa-db-fra.com

Comment puis-je avoir la sortie du journal de l'enregistreur Ruby sur stdout ainsi que sur fichier?

Quelque chose comme une fonctionnalité de té dans l'enregistreur.

83
Manish Sapariya

Vous pouvez écrire une pseudo classe IO qui écrira dans plusieurs objets IO. 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

Puis définissez-le comme fichier journal:

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

Chaque fois que Logger appelle puts sur votre objet MultiIO, il écrira à la fois STDOUT et dans votre fichier journal.

Edit: Je suis allé de l'avant et compris le reste de l'interface. Un périphérique de journalisation doit répondre à write et à close (pas puts). Tant que MultiIO répond à ceux-ci et les envoie aux vrais IO objets, cela devrait fonctionner.

111
David

@ 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)
46
jonas054

Si vous êtes dans Rails 3 ou 4, comme cet article de blog souligne, Rails 4 a cette fonctionnalité intégrée . Alors tu peux 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 faire en backport:

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

Bien que j'aime bien les autres suggestions, j'ai constaté que j'avais le même problème, mais je voulais pouvoir utiliser différents niveaux de journalisation pour STDERR et le fichier (comme je le pouvais avec les plus grands cadres de journalisation tels que NLog). Je me suis retrouvé avec une stratégie de routage qui multiplexe au niveau de l'enregistreur plutôt qu'au niveau IO, afin que chaque enregistreur puisse ensuite 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 )
11
dsz

Vous pouvez également ajouter plusieurs fonctionnalités de journalisation de périphérique directement dans le consignateur:

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')
10
Ramon de C Valle

Pour ceux qui aiment ça simple:

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 de l'enregistreur:

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

En fait, j'utilise cette technique pour imprimer dans un fichier journal, un service de journalisation dans le cloud (entrées) et, dans le cas d'un environnement de développement, également dans STDOUT.

9
Igor

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

Ceci utilise un modèle similaire à Delegator . Ainsi, vous n'avez pas à répertorier toutes les méthodes que vous souhaitez déléguer, car toutes les méthodes définies dans l'un des objets cibles seront déléguées:

class Tee < DelegateToAllClass(IO)
end

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

Vous devriez également pouvoir utiliser cela avec Logger.

delegate_to_all.rb est disponible à partir d'ici: https://Gist.github.com/TylerRick/4990898

9
Tyler Rick

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

Il s'agit de la même implémentation, mais en utilisant des classes anonymes afin 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 méthode de pollution avec l'implémentation d'origine, contrastant avec 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. tee a une méthode write, mais pas de méthode size comme prévu. Maintenant, considérons lorsque 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, tee2 répond à size comme prévu, mais il répond également à write en raison du premier délégué. Même tee répond maintenant à size en raison de la pollution de la méthode. 

Contrairement à la solution de classe anonyme, tout se passe 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
3
Rado

Ê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.

2
knut

Rapide et sale (ref: https://coderwall.com/p/y_b3ra/log-to-stdout-and-a-file-at-the-same-time )

require 'logger'
ll=Logger.new('| tee script.log')
ll.info('test')
2
Jose Alban

Je suis allé à la même idée de "Déléguer toutes les méthodes à des sous-éléments" que d'autres personnes avaient déjà explorée, mais je retourne pour chacune d'elles la valeur de retour du dernier appel de la méthode . Si je ne le faisais pas, c'est logger-colors cassé qui attendaient une Integer et la carte renvoyait une 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 va redéléguer chaque méthode à toutes les cibles et renvoyer uniquement la valeur de retour du dernier appel.

De plus, si vous voulez des couleurs, STDOUT ou STDERR doit être mis en dernier lieu, car ce sont les deux seules où les couleurs sont supposées être sorties. Mais alors, il va également sortir 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
Jerska

J'ai écrit un petit RubyGem qui vous permet de faire plusieurs 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
Patrick Hüsler

Une autre 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
Michael Voigt

Ceci est 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 la nécessité d'un wrapper de classe externe. C'est un utilitaire utile à avoir dans un fichier Ruby séparé.

Utilisez-le comme une ligne 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
Charles Murphy

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 l’enregistreur Ruby sur stdout et 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')

Note Je sais que cela ne répond pas directement à la question, mais que cela est étroitement lié. Chaque fois que je cherchais une sortie sur plusieurs IO, je rencontrais ce fil de discussion. J'espère que vous trouverez cela utile aussi.

0
knugie

Une autre 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 façon.

# 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 tags 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 ça aide quelqu'un.

0
retgoat