Ruby logger günlük çıktısını stdout'a ve dosyaya nasıl alabilirim?


Yanıtlar:


124

IOBirden çok IOnesneye yazacak sözde bir sınıf yazabilirsiniz . Gibi bir şey:

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

Sonra bunu günlük dosyanız olarak ayarlayın:

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

Nesnenizi her Loggerçağırdığınızda puts, MultiIOhem STDOUTgünlük dosyanıza hem de günlük dosyanıza yazacaktır .

Düzenleme: Devam ettim ve arayüzün geri kalanını çözdüm. Bir günlük cihazı yanıt vermeli writeve close( yanıtlamamalıdır puts). MultiIOBunlara yanıt verdiği ve onları gerçek IO nesnelerine temsil ettiği sürece , bu işe yaramalıdır.


logger'ın ctor'una bakarsanız, bunun log rotasyonunu bozacağını göreceksiniz. 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
Ruby 2.2'deki not @targets.each(&:close), amortismana tabi tutulmuştur.
xis

Düzenli olarak çağırmam gerektiğini fark edene kadar benim için çalıştı: log_file'ı kapatmak için log_file'ı almak ve logger'ın günlüğe kaydettiğini güncellemek için (esasen bir "kaydetme"). STDOUT hoşuna gitmedi: ona çağrılmaktan, MultoIO fikrini bir nevi yenmek gibi. Atlamak için bir hack eklendi: class File dışında close, ama keşke daha zarif bir çözümüm olsaydı.
Kim Miller

48

@ David'in çözümü çok iyi. Koduna göre birden çok hedef için genel bir temsilci sınıfı oluşturdum.

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)

Bunun nasıl daha iyi olduğunu veya bu yaklaşımın geliştirilmiş faydalarının David tarafından önerilen düz olandan daha iyi olduğunu açıklar
mısınız

5
Endişelerin ayrılması. MultiDelegator yalnızca çağrıları birden çok hedefe devretmeyi bilir. Bir kayıt cihazının bir yazma ve kapatma yöntemine ihtiyaç duyduğu gerçeği, arayan kişiye uygulanır. Bu, MultiDelegator'ı günlük kaydı dışında başka durumlarda kullanılabilir hale getirir.
jonas054

Güzel çözüm. Bunu tırmık görevlerimden bir günlük dosyasına çıktı almak için kullanmayı denedim. Yine de puts ile çalışmasını sağlamak için ($ stdout.puts'u "özel yöntem" koyar "çağrısı almadan çağırabilmek için), birkaç yöntem daha eklemem gerekiyordu: log_file = File.open (" tmp / rake.log "," a ") $ stdout = MultiDelegator.delegate (: write,: close,: puts,: print) .to (STDOUT, log_file) 'den miras alınan bir Tee sınıfı oluşturmak mümkün olsaydı iyi olurdu MultiDelegator, sen stdlib içinde Yetkilendirene sınıfında ... ile gerçekleştirebildiğiniz
Tyler Rick

Bunun DelegatorToAll adını verdiğim Delegator benzeri bir uygulamasını buldum. Bu şekilde, temsilci sınıfında (IO) tanımlanan tüm yöntemleri delege edeceğinden, yetkilendirmek istediğiniz tüm yöntemleri listelemeniz gerekmez: class Tee <DelegateToAllClass (IO) end $ stdout = Tee.new (STDOUT , File.open ("# { FILE } .log", "a")) Daha fazla ayrıntı için gist.github.com/TylerRick/4990898 adresine bakın.
Tyler Rick

1
Çözümünüzü gerçekten beğendim, ancak her delegasyon tüm örnekleri yeni yöntemlerle kirlettiği için birden çok kez kullanılabilen genel bir temsilci olarak iyi değil. Bu sorunu gideren bir cevap ( stackoverflow.com/a/36659911/123376 ) gönderdim . Örnekler de yayınladığım gibi iki uygulama arasındaki farkı görmek eğitici olabileceğinden, bir düzenleme yerine bir yanıt gönderdim.
Rado

35

Rails 3 veya 4'teyseniz, bu blog gönderisinin de işaret ettiği gibi, Rails 4 bu işlevselliğe sahiptir . Böylece şunları yapabilirsiniz:

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

Veya Rails 3'teyseniz, onu arka plana atabilirsiniz:

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

Bu, rayların dışında mı yoksa yalnızca raylar için mi geçerli?
Ed Sykes

ActiveSupport'a dayalıdır, bu nedenle bu bağımlılığa zaten sahipseniz, yukarıda gösterildiği gibi extendherhangi bir ActiveSupport::Loggerörneği yapabilirsiniz .
phillbaker

Teşekkürler, yardımcı oldu.
Lucas

config.logger.extend()İç ortam yapılandırmamı kullanırken bazı tuhaflıklar yaşasa da, bunun en basit ve en etkili cevap olduğunu düşünüyorum . Bunun yerine, set config.loggeriçin STDOUTo sırada, çevrede farklı başlatıcılar logger uzatıldı.
mattsch

14

Basitten hoşlananlar için:

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

kaynak

Veya mesajı Logger formatlayıcıda yazdırın:

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

Aslında bu tekniği bir günlük dosyasına, bir bulut günlükçü hizmetine (log girişleri) ve geliştirme ortamı ise - STDOUT'a yazdırmak için kullanıyorum.


2
"| tee test.log"eski çıktıların üzerine "| tee -a test.log"
fangxing

13

Diğer önerileri oldukça beğenmeme rağmen, aynı sorunu yaşadığımı fark ettim ancak STDERR ve dosya için farklı kayıt seviyelerine sahip olmak istedim.

Sonunda, her bir kaydedicinin bağımsız günlük seviyelerinde çalışabilmesi için, GÇ düzeyi yerine günlükçü düzeyinde çoğullayan bir yönlendirme stratejisi buldum:

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
Bu çözümü en çok (1) basit olduğu için seviyorum ve (2) her şeyin bir dosyaya gideceğini varsaymak yerine Logger sınıflarınızı yeniden kullanmaya teşvik ediyor. Benim durumumda, STDOUT'a ve Graylog için bir GELF ekleyicisine giriş yapmak istiyorum. MultiLogger@ Dsz'nin tanımladığı gibi olması mükemmel bir uyumdur. Paylaşım için teşekkürler!
Eric Kramer

Sahte değişkenleri (ayarlayıcılar / alıcılar) işlemek için bölüm eklendi
Eric Kramer

12

Ayrıca, doğrudan Logger'a birden fazla cihaz günlüğü işlevi ekleyebilirsiniz:

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

Örneğin:

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

İşte @ jonas054'ün cevabından esinlenen başka bir uygulama .

Bu, benzer bir kalıp kullanır Delegator. Bu şekilde, herhangi bir hedef nesnede tanımlanan tüm yöntemleri delege edeceğinden, yetkilendirmek istediğiniz tüm yöntemleri listelemeniz gerekmez:

class Tee < DelegateToAllClass(IO)
end

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

Bunu Logger ile de kullanabilmelisiniz.

delegate_to_all.rb'ye buradan ulaşılabilir: https://gist.github.com/TylerRick/4990898



3

@ jonas054'ün yukarıdaki cevabı harika, ancak MultiDelegatorher yeni delege ile sınıfı kirletiyor . MultiDelegatorBirkaç kez kullanırsanız , istenmeyen bir durum olan sınıfa yöntemler eklemeye devam edecektir. (Örneğin aşağıya bakınız)

İşte aynı uygulama, ancak anonim sınıflar kullanmak, böylece yöntemler temsilci sınıfını kirletmez.

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

Aşağıda, değiştirilmiş uygulamaya zıt olarak orijinal uygulama ile yöntem kirliliğinin bir örneği verilmiştir:

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

Yukarıda her şey güzel. teebir writeyöntemi var, ancak sizebeklendiği gibi bir yöntemi yok . Şimdi, başka bir temsilci oluşturduğumuzda düşünün:

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

Oh hayır, beklendiği gibi tee2yanıt veriyor size, ama aynı zamanda writeilk delege yüzünden de yanıt veriyor . teeŞimdi bile sizeyöntem kirliliği nedeniyle yanıt veriyor .

Bunu anonim sınıf çözümüyle karşılaştırın, her şey beklendiği gibi:

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

Standart kaydedici ile sınırlı mısınız?

Değilse log4r kullanabilirsiniz :

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

Bir avantaj: Standart çıkış ve dosya için farklı günlük düzeyleri de tanımlayabilirsiniz.


1

Diğer insanların zaten keşfettiği "Tüm yöntemleri alt öğelere devretme" fikrine gittim, ancak her biri için yöntemin son çağrısının dönüş değerini döndürüyorum. Ben yapmadım, o kırdı logger-colorsbir bekliyorduk hangi Integerve harita bir dönüyordu 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

Bu, her yöntemi tüm hedeflere yeniden atar ve yalnızca son çağrının dönüş değerini döndürür.

Ayrıca, renk istiyorsanız, STDOUT veya STDERR en son yerleştirilmelidir, çünkü yalnızca iki renk çıktısı olması gerekirdi. Ancak daha sonra, dosyanıza renkler de çıkarır.

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

1

Aşağıdakilerden birkaçını yapmanıza izin veren küçük bir RubyGem yazdım:

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

Kodu github'da bulabilirsiniz: teerb


1

Bir yol daha. Etiketli günlük kaydı kullanıyorsanız ve başka bir günlük dosyasında da etiketlere ihtiyacınız varsa, bunu bu şekilde yapabilirsiniz.

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

Bundan sonra, alternatif kaydedicide uuid etiketleri alacaksınız

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

Umarım bu birine yardımcı olur.


Basit, güvenilir ve mükemmel çalışıyor. Teşekkürler! Not ActiveSupport::Loggersadece kullanmak gerekir - bu kutunun dışında çalışır Rails.logger.extendile ActiveSupport::Logger.broadcast(...).
XtraSimplicity

0

Bir seçenek daha ;-)

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

Sevdiğim MultiIO yaklaşım. Ruby Logger ile iyi çalışır . Saf GÇ kullanırsanız , GÇ nesnelerinin sahip olması beklenen bazı yöntemlerden yoksun olduğu için çalışmayı durdurur. Borulardan daha önce burada bahsedilmişti: Ruby logger log çıktısını stdout'a ve dosyaya nasıl alabilirim? . İşte benim için en iyi olan şey.

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

Not Bunun soruyu doğrudan yanıtlamadığını biliyorum, ancak büyük ölçüde ilişkili. Ne zaman çoklu IO'lara çıktı aradığımda bu konuya rastladım ve umarım bunu da faydalı bulursunuz.


0

Bu, @ rado'nun çözümünün basitleştirilmesidir.

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

Dış sınıf paketleyiciye ihtiyaç duymadan onun ile aynı faydalara sahiptir. Ayrı bir Ruby dosyasında bulunması yararlı bir yardımcı programdır.

Aşağıdaki gibi temsilci örnekleri oluşturmak için tek satırlık bir uygulama olarak kullanın:

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

VEYA bunu bir fabrika olarak kullanın:

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

Gem'deki Loog::Teenesneyi kullanabilirsiniz loog:

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

Tam olarak aradığınız şey.


0

Kullanmakta sorun yaşamıyorsanız, bir günlükçüye ek günlük hedefleri eklemenin mükemmel ve çok kısa bir yolu olan ActiveSupportkontrol etmenizi şiddetle tavsiye ederim ActiveSupport::Logger.broadcast.

Aslında, Rails 4+ kullanıyorsanız ( bu işlemden itibaren ), istenen davranışı elde etmek için hiçbir şey yapmanıza gerek yoktur - en azından rails console. Eğer her kullandığınızda rails console, Raylar otomatik uzanan Rails.loggero (olağan dosya hedefine hem çıkışı böyle log/production.logörneğin) ve 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

Bilinmeyen ve talihsiz bir nedenden ötürü, bu yöntem belgelenmemiştir ancak nasıl çalıştığını öğrenmek veya örnekleri görmek için kaynak koduna veya blog yazılarına başvurabilirsiniz .

https://www.joshmcarthur.com/til/2018/08/16/logging-to-multiple-destinations-using-activesupport-4.html'de başka bir örnek var:

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

Ayrıca son zamanlarda bu ihtiyacım var, bu yüzden bunu yapan bir kütüphane uyguladım. Bu StackOverflow sorusunu yeni keşfettim, bu yüzden ihtiyacı olan herkes için oraya koyuyorum: https://github.com/agis/multi_io .

Burada bahsedilen diğer çözümlerle karşılaştırıldığında, bu IOkendi başına bir nesne olmaya çalışır, bu nedenle diğer normal IO nesneleri (dosyalar, soketler vb.) İçin bir drop-in yerine kullanılabilir.

Bununla birlikte, henüz tüm standart GÇ yöntemlerini uygulamadım, ancak GÇ anlamını takip edenler (örneğin, #writetüm temel GÇ hedeflerine yazılan bayt sayısının toplamını döndürür).


-3

STDOUT'unuzun kritik çalışma zamanı bilgileri ve oluşan hatalar için kullanıldığını düşünüyorum.

Bu yüzden kullanıyorum

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

hata ayıklama ve düzenli günlüğe kaydetme günlüğünü ve ardından birkaç

  puts "doing stuff..."

komut dosyalarımın çalıştığına dair STDOUT bilgilerini görmem gereken yer!

Bah, sadece 10 sentim :-)

Sitemizi kullandığınızda şunları okuyup anladığınızı kabul etmiş olursunuz: Çerez Politikası ve Gizlilik Politikası.
Licensed under cc by-sa 3.0 with attribution required.