Quelque chose comme une fonctionnalité de té dans l'enregistreur.
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.
@ 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)
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))
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 )
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')
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
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.
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
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
Ê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.
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')
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"
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
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 ...')
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")
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.
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.