Bagaimana saya bisa memiliki output log ruby ​​logger ke stdout serta file?


Jawaban:


124

Anda dapat menulis IOkelas semu yang akan menulis ke banyak IOobjek. Sesuatu seperti:

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

Kemudian atur itu sebagai file log Anda:

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

Setiap kali Loggermemanggil objek putsAnda MultiIO, itu akan menulis ke keduanya STDOUTdan file log Anda.

Sunting: Saya melanjutkan dan menemukan sisa antarmuka. Perangkat log harus merespons writedan close(tidak puts). Selama MultiIOmenanggapi itu dan memproksinya ke objek IO yang sebenarnya, ini harus berfungsi.


jika Anda melihat ctor logger Anda akan melihat bahwa ini akan mengacaukan rotasi log. 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
Catatan di Ruby 2.2, @targets.each(&:close)disusutkan.
xis

Bekerja untuk saya sampai saya menyadari bahwa saya perlu secara berkala memanggil: tutup pada log_file agar log_file memperbarui apa yang telah dicatat logger (pada dasarnya "simpan"). STDOUT tidak suka: hampir dipanggil, semacam mengalahkan ide MultoIO. Menambahkan peretasan untuk melewati: tutup kecuali untuk File kelas, tetapi berharap saya memiliki solusi yang lebih elegan.
Kim Miller

48

Solusi @ David sangat bagus. Saya telah membuat kelas delegator generik untuk beberapa target berdasarkan kodenya.

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)

Bisakah Anda menjelaskan, bagaimana ini lebih baik atau apa kegunaan yang disempurnakan dari pendekatan ini daripada yang biasa disarankan oleh David
Manish Sapariya

5
Ini pemisahan perhatian. MultiDelegator hanya tahu tentang mendelegasikan panggilan ke beberapa target. Fakta bahwa perangkat logging memerlukan metode tulis dan tutup diterapkan di pemanggil. Ini membuat MultiDelegator dapat digunakan dalam situasi selain logging.
jonas054

Solusi bagus. Saya mencoba menggunakan ini untuk mengirim output dari tugas menyapu saya ke file log. Untuk membuatnya berfungsi dengan put meskipun (agar dapat memanggil $ stdout.puts tanpa mendapatkan "metode pribadi` put 'dipanggil "), saya harus menambahkan beberapa metode lagi: log_file = File.open (" tmp / rake.log "," a ") $ stdout = MultiDelegator.delegate (: write,: close,: put,: print) .to (STDOUT, log_file) Akan lebih baik jika memungkinkan untuk membuat kelas Tee yang diwarisi dari MultiDelegator, seperti yang dapat Anda lakukan dengan kelas Delegator di stdlib ...
Tyler Rick

Saya datang dengan implementasi seperti Delegator dari ini yang saya sebut DelegatorToAll. Dengan cara ini Anda tidak perlu membuat daftar semua metode yang ingin Anda delegasikan, karena ini akan mendelegasikan semua metode yang didefinisikan di kelas delegasi (IO): class Tee <DelegateToAllClass (IO) end $ stdout = Tee.new (STDOUT , File.open ("# { FILE } .log", "a")) Lihat gist.github.com/TylerRick/4990898 untuk detail selengkapnya.
Tyler Rick

1
Saya sangat menyukai solusi Anda, tetapi tidak baik sebagai delegator umum yang dapat digunakan berkali-kali karena setiap delegasi mencemari semua instance dengan metode baru. Saya memposting jawaban di bawah ( stackoverflow.com/a/36659911/123376 ) yang memperbaiki masalah ini. Saya memposting jawaban daripada edit karena mungkin mendidik untuk melihat perbedaan antara dua implementasi karena saya juga memposting contoh.
Rado

35

Jika Anda berada di Rails 3 atau 4, seperti yang ditunjukkan oleh posting blog ini , Rails 4 memiliki fungsi bawaan ini . Jadi Anda bisa melakukan:

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

Atau jika Anda menggunakan Rails 3, Anda dapat melakukan 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))

apakah ini berlaku di luar rel, atau hanya rel?
Ed Sykes

Ini didasarkan pada ActiveSupport, jadi jika Anda sudah memiliki ketergantungan itu, Anda bisa contoh extendapa pun ActiveSupport::Loggerseperti yang ditunjukkan di atas.
pembuat phillbaker

Terima kasih, ini sangat membantu.
Lucas

Saya pikir ini adalah jawaban yang paling sederhana dan efektif, meskipun saya memiliki beberapa keanehan menggunakan config.logger.extend()konfigurasi di dalam lingkungan saya. Sebaliknya, saya mengatur config.loggerke STDOUTdalam lingkungan saya, kemudian memperpanjang logger di penginisialisasi yang berbeda.
mattsch

14

Bagi yang suka sederhana:

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

sumber

Atau cetak pesan di pemformat 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

Saya sebenarnya menggunakan teknik ini untuk mencetak ke file log, layanan cloud logger (logentries) dan jika itu lingkungan dev - juga mencetak ke STDOUT.


2
"| tee test.log"akan menimpa keluaran lama, mungkin sebagai "| tee -a test.log"gantinya
fangxing

13

Meskipun saya cukup menyukai saran lainnya, saya menemukan bahwa saya memiliki masalah yang sama tetapi menginginkan kemampuan untuk memiliki tingkat logging yang berbeda untuk STDERR dan file.

Saya berakhir dengan strategi perutean yang multipleks di tingkat logger daripada di tingkat IO, sehingga setiap logger kemudian dapat beroperasi di tingkat log independen:

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
Saya paling suka solusi ini karena (1) sederhana, dan (2) mendorong Anda untuk menggunakan kembali kelas Logger Anda alih-alih mengasumsikan semuanya masuk ke file. Dalam kasus saya, saya ingin login ke STDOUT dan appender GELF untuk Graylog. Memiliki MultiLoggersuka yang dijelaskan @dsz sangat cocok. Terima kasih telah berbagi!
Eric Kramer

Menambahkan bagian untuk menangani pseudovariabel (setter / getter)
Eric Kramer

11

Anda juga dapat menambahkan fungsionalitas pencatatan beberapa perangkat langsung ke Logger:

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

Misalnya:

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

Berikut implementasi lainnya, terinspirasi oleh jawaban @ jonas054 .

Ini menggunakan pola yang mirip dengan Delegator. Dengan cara ini Anda tidak perlu membuat daftar semua metode yang ingin Anda delegasikan, karena ini akan mendelegasikan semua metode yang didefinisikan di salah satu objek target:

class Tee < DelegateToAllClass(IO)
end

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

Anda harus dapat menggunakan ini dengan Logger juga.

delegate_to_all.rb tersedia dari sini: https://gist.github.com/TylerRick/4990898



3

Jawaban @ jonas054 di atas bagus, tetapi mengotori MultiDelegatorkelas dengan setiap delegasi baru. Jika Anda menggunakan MultiDelegatorbeberapa kali, itu akan terus menambahkan metode ke kelas, yang tidak diinginkan. (Lihat di bawah untuk contoh)

Berikut adalah implementasi yang sama, tetapi menggunakan kelas anonim sehingga metode tidak mencemari kelas delegator.

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

Berikut adalah contoh metode pencemaran dengan implementasi asli, berbeda dengan implementasi yang dimodifikasi:

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

Semuanya bagus di atas. teememiliki writemetode, tetapi tidak ada sizemetode seperti yang diharapkan. Sekarang, pertimbangkan ketika kita membuat delegasi lain:

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

Oh tidak, tee2menanggapi sizeseperti yang diharapkan, tetapi juga menanggapi writekarena delegasi pertama. Bahkan teesekarang menanggapi sizekarena metode pencemaran.

Bandingkan ini dengan solusi kelas anonim, semuanya seperti yang diharapkan:

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

Apakah Anda dibatasi untuk logger standar?

Jika tidak, Anda dapat menggunakan 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

Satu keuntungan: Anda juga dapat menentukan tingkat log yang berbeda untuk stdout dan file.


1

Saya pergi ke ide yang sama tentang "Mendelegasikan semua metode ke sub-elemen" yang sudah dieksplorasi orang lain, tetapi saya mengembalikan untuk masing-masing metode tersebut nilai kembalian dari panggilan terakhir metode tersebut. Jika saya tidak, itu rusak logger-colorsyang mengharapkan Integerdan peta kembali 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

Ini akan mendelegasikan ulang setiap metode ke semua target, dan hanya mengembalikan nilai kembali dari panggilan terakhir.

Juga, jika Anda ingin warna, STDOUT atau STDERR harus diletakkan terakhir, karena hanya dua warna yang seharusnya menjadi keluaran. Tapi kemudian, itu juga akan menampilkan warna ke file Anda.

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

1

Saya telah menulis RubyGem kecil yang memungkinkan Anda melakukan beberapa hal ini:

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

Anda dapat menemukan kode di github: teerb


1

Satu cara lagi. Jika Anda menggunakan logging yang diberi tag dan juga memerlukan tag di file log lain, Anda dapat melakukannya dengan cara ini

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

Setelah ini Anda akan mendapatkan tag uuid di logger 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

Harapan yang membantu seseorang.


Sederhana, andal, dan bekerja dengan sangat baik. Terima kasih! Perhatikan bahwa ActiveSupport::Loggerbekerja di luar kotak dengan ini - Anda hanya perlu menggunakan Rails.logger.extenddengan ActiveSupport::Logger.broadcast(...).
XtraSimplicity

0

Satu opsi lagi ;-)

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

Saya suka pendekatan MultiIO . Ini bekerja dengan baik dengan Ruby Logger . Jika Anda menggunakan IO murni, ia berhenti bekerja karena tidak memiliki beberapa metode yang diharapkan dimiliki oleh objek IO. Pipa disebutkan sebelumnya di sini: Bagaimana saya bisa memiliki output log ruby ​​logger ke stdout serta file? . Inilah yang paling cocok untuk saya.

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

Catatan Saya tahu ini tidak menjawab pertanyaan secara langsung tetapi sangat terkait. Setiap kali saya mencari output ke beberapa IO, saya menemukan utas ini. Jadi, saya harap ini juga berguna bagi Anda.


0

Ini adalah penyederhanaan dari solusi @ 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

Ini memiliki semua manfaat yang sama seperti miliknya tanpa perlu pembungkus kelas luar. Ini adalah utilitas yang berguna untuk dimiliki dalam file ruby ​​terpisah.

Gunakan sebagai one-liner untuk menghasilkan instance delegator seperti ini:

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

ATAU gunakan sebagai pabrik seperti:

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

Anda dapat menggunakan Loog::Teeobjek dari loogpermata:

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

Persis apa yang Anda cari.


0

Jika Anda tidak keberatan menggunakannya ActiveSupport, saya akan sangat menyarankan untuk memeriksa ActiveSupport::Logger.broadcast, yang merupakan cara yang sangat baik dan ringkas untuk menambahkan tujuan log tambahan ke logger.

Faktanya, jika Anda menggunakan Rails 4+ (seperti dari commit ini ), Anda tidak perlu melakukan apa pun untuk mendapatkan perilaku yang diinginkan - setidaknya jika Anda menggunakan rails console. Setiap kali Anda menggunakan rails console, Rails secara otomatis meluas Rails.loggersedemikian rupa sehingga menghasilkan output ke tujuan file biasanya ( log/production.log, misalnya) dan 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

Untuk beberapa alasan yang tidak diketahui dan disayangkan, metode ini tidak berdokumen tetapi Anda dapat merujuk ke kode sumber atau posting blog untuk mempelajari cara kerjanya atau melihat contoh.

https://www.joshmcarthur.com/til/2018/08/16/logging-to-multiple-destinations-using-activesupport-4.html memiliki contoh lain:

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

Saya juga memiliki kebutuhan ini baru-baru ini, jadi saya menerapkan perpustakaan yang melakukan ini. Saya baru saja menemukan pertanyaan StackOverflow ini, jadi saya menaruhnya untuk siapa saja yang membutuhkannya: https://github.com/agis/multi_io .

Dibandingkan dengan solusi lain yang disebutkan di sini, ini berusaha untuk menjadi IOobjeknya sendiri, sehingga dapat digunakan sebagai pengganti drop-in untuk objek IO reguler lainnya (file, soket, dll.)

Meskipun demikian, saya belum mengimplementasikan semua metode IO standar, tetapi yang mengikuti semantik IO (misalnya, #writemengembalikan jumlah dari jumlah byte yang ditulis ke semua target IO yang mendasarinya).


-3

Saya pikir STDOUT Anda digunakan untuk info runtime kritis dan kesalahan yang diangkat.

Jadi saya gunakan

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

untuk log debug dan logging biasa, dan kemudian menulis beberapa

  puts "doing stuff..."

di mana saya perlu melihat informasi STDOUT bahwa skrip saya berjalan sama sekali!

Bah, hanya 10 sen saya :-)

Dengan menggunakan situs kami, Anda mengakui telah membaca dan memahami Kebijakan Cookie dan Kebijakan Privasi kami.
Licensed under cc by-sa 3.0 with attribution required.