Quelque chose comme une fonctionnalité de tee dans Logger.
Quelque chose comme une fonctionnalité de tee dans Logger.
tee --append test.log
pour empêcher les écrasements.
Réponses:
Vous pouvez écrire une pseudo IO
classe qui écrira sur plusieurs IO
objets. 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 Logger
appelez puts
votre MultiIO
objet, il écrira à la fois dans STDOUT
votre 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 à write
et close
(pas puts
). Tant que MultiIO
répond à ceux-ci et les envoie par proxy aux objets d'E / S réels, cela devrait fonctionner.
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
@targets.each(&:close)
est amorti.
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 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))
extend
n'importe quelle ActiveSupport::Logger
instance comme indiqué ci-dessus.
config.logger.extend()
configuration de mon environnement. Au lieu de cela, je me mis config.logger
à STDOUT
dans mon environnement, puis étendu l'enregistreur dans différentes initializers.
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
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.
"| tee test.log"
remplacera les anciennes sorties, peut-être à la "| tee -a test.log"
place
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)
MultiLogger
comme @dsz décrit est un excellent choix. Merci d'avoir partagé!
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')
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
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')
La réponse de @ jonas054 ci-dessus est excellente, mais elle pollue la MultiDelegator
classe à 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)
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. tee
a une write
méthode, mais aucune size
mé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, tee2
répond size
comme prévu, mais il répond aussi à write
cause du premier délégué. Même tee
maintenant répond à size
cause 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
Ê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.
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-colors
et attendait un Integer
et 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"
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
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.
ActiveSupport::Logger
fonctionne hors de la boîte avec cela - il vous suffit de l'utiliser Rails.logger.extend
avec ActiveSupport::Logger.broadcast(...)
.
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 ...')
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.
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")
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.log
par 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"
…
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 IO
objet à 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, #write
retourne la somme du nombre d'octets écrits sur toutes les cibles IO sous-jacentes).
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 :-)
| tee
avant le fichier a fonctionné pour moi, doncLogger.new("| tee test.log")
. Notez le tuyau. Cela provenait