Rubyロガーのログをstdoutだけでなくファイルにも出力するにはどうすればよいですか?


回答:


124

IO複数のIOオブジェクトに書き込む疑似クラスを作成できます。何かのようなもの:

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

次に、それをログファイルとして設定します。

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

オブジェクトをLogger呼び出すたびputsに、MultiIOオブジェクトSTDOUTとログファイルの両方に書き込まれます。

編集:私は先に進んで、残りのインターフェイスを理解しました。ログデバイスはwriteclose(ではなくputs)に応答する必要があります。これらにMultiIO応答し、それらを実際のIOオブジェクトにプロキシする限り、これは機能するはずです。


ロガーの俳優を見ると、これがログのローテーションを台無しにすることがわかります。 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 2014

3
Ruby 2.2での注記@targets.each(&:close)は廃止されました。
xis 2015年

ロガーがログに記録したものを更新するためにlog_fileを取得するために:closeをlog_fileで定期的に呼び出す必要があることに気づくまで、私のために働きました(本質的には「保存」)。STDOUTは、:closeが呼び出されるのが嫌いで、MultoIOのアイデアを打ち負かしています。クラスFileを除いて:closeをスキップするハックを追加しましたが、もっとエレガントな解決策があったらいいのにと思います。
Kim Miller

48

@Davidのソリューションは非常に優れています。彼のコードに基づいて、複数のターゲット用の汎用デリゲータークラスを作成しました。

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)

これがどのように優れているか、またはこのアプローチのユーティリティの拡張が、Davidによって提案されたプレーンなものよりも優れていることを説明してください
Manish Sapariya

5
それは懸念の分離です。MultiDelegatorは、複数のターゲットへの呼び出しの委任についてのみ認識しています。ロギングデバイスが書き込みとクローズメソッドを必要とするという事実は、呼び出し側に実装されています。これにより、ロギング以外の状況でMultiDelegatorを使用できるようになります。
jonas054 11/07/14

素晴らしい解決策。これを使用して、rakeタスクからの出力をログファイルにティーしようとしました。putsで動作するようにするには(「プライベートメソッド `puts 'が呼び出されない」で$ stdout.putsを呼び出せるようにするため)、さらにいくつかのメソッドを追加する必要がありました。log_file = File.open(" tmp / rake.log "、" a ")$ stdout = MultiDelegator.delegate(:write、:close、:puts、:print).to(STDOUT、log_file)から継承されたTeeクラスを作成できればよいでしょうMultiDelegator、stdlibのDelegatorクラスで実行できるように
Tyler Rick

私はDelegatorToAllと呼ばれるこれのデリゲーターのような実装を思いつきました。この方法では、デリゲートするすべてのメソッドをリストする必要はありません。デリゲートクラス(IO)で定義されているすべてのメソッドがデリゲートされるためです。class Tee <DelegateToAllClass(IO)end $ stdout = Tee.new(STDOUT 、File.open( "#{ FILE } .log"、 "a"))詳細については、gist.github.com / TylerRick / 4990898を参照してください。
タイラーリック

1
私は本当にあなたのソリューションが好きですが、すべての委任がすべてのインスタンスを新しいメソッドで汚染するため、複数回使用できる汎用的な委任者としては良くありません。この問題を修正する以下の回答(stackoverflow.com/a/36659911/123376)を投稿しました。私も例を投稿したように、2つの実装の違いを確認することは教育的である可能性があるため、編集ではなく回答を投稿しました。
ラドー

35

Rails 3または4を使用している場合、このブログ投稿が指摘するように、Rails 4にはこの機能が組み込まれています。だからあなたはできる:

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

または、Rails 3を使用している場合は、それをバックポートできます。

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

これはレールの外側に適用できますか、それともレールのみに適用できますか?
Ed Sykes、

これはActiveSupportに基づいているため、すでにその依存関係がある場合は、上記のようにextend任意のActiveSupport::Loggerインスタンスを使用できます。
phillbaker 2015年

おかげで、役に立ちました。
Lucas

私はこれが最も簡単で最も効果的な答えだと思いますが、config.logger.extend()私の環境設定の内部を使用していると、奇妙なことがありました。代わりに、自分の環境でに設定してconfig.loggerからSTDOUT、ロガーをさまざまな初期化子で拡張しました。
mattsch 2017

14

シンプルなものが好きな人のために:

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

ソース

または、ロガーフォーマッタでメッセージを印刷します。

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

私は実際にこの手法を使用して、ログファイル、クラウドロガーサービス(ログエントリ)、およびそれが開発環境の場合はSTDOUTに出力します。


2
"| tee test.log"古い出力を上書きしますが、"| tee -a test.log"代わりに可能性があります
18年

13

他の提案は非常に気に入っていますが、同じ問題があることがわかりましたが、STDERRとファイルに異なるログレベルを設定する機能が必要でした。

各ロガーが独立したログレベルで動作できるように、IOレベルではなくロガーレベルで多重化するルーティング戦略になりました。

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
このソリューションは、(1)シンプルであり、(2)すべてがファイルに送られると想定するのではなく、Loggerクラスを再利用することを奨励しているので、私はこのソリューションが一番好きです。私の場合、GraylogのSTDOUTとGELFアペンダーにログを記録したいと思います。持つMultiLoggerよう@dszは素晴らしいフィット感で説明します。共有してくれてありがとう!
Eric Kramer

疑似変数(セッター/ゲッター)を処理するセクションを追加
Eric Kramer

11

複数のデバイスロギング機能をロガーに直接追加することもできます。

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

例えば:

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

@ jonas054の回答に触発された別の実装があります。

これはに類似したパターンを使用しDelegatorます。この方法では、ターゲットオブジェクトのいずれかで定義されているすべてのメソッドを委任するため、委任するすべてのメソッドをリストする必要はありません。

class Tee < DelegateToAllClass(IO)
end

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

ロガーでもこれを使用できるはずです。

delegate_to_all.rbはこちらから入手できます:https : //gist.github.com/TylerRick/4990898



3

上記の@ jonas054の回答はすばらしいですが、MultiDelegator新しいデリゲートを使用するたびにクラスを汚染します。MultiDelegator複数回使用すると、クラスにメソッドが追加され続けるため、望ましくありません。(下の例を参照)

これは同じ実装ですが、メソッドがデリゲータクラスを汚染しないように匿名クラスを使用しています。

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

変更された実装とは対照的に、元の実装によるメソッド汚染の例を次に示します。

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

上記はすべて問題ありません。tee持っているwrite方法、ないsize予想通りの方法を。ここで、別のデリゲートを作成するときを考えます。

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

ああ、いや、tee2に応答size予想通り、それはまたに応答writeので、最初のデリゲートの。メソッド汚染のためtee今でも対応しsizeます。

これを匿名のクラスソリューションと比較すると、すべてが期待どおりです。

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

標準のロガーに制限されていますか?

そうでない場合は、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

1つの利点:stdoutとfileに異なるログレベルを定義することもできます。


1

「すべてのメソッドをサブエレメントに委任する」という他の人々がすでに検討していたのと同じ考え方に行きましたが、それぞれにメソッドの最後の呼び出しの戻り値を返しています。私がしなかった場合、それは壊れlogger-colorsていて、Integerマップがを返していました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

これにより、すべてのメソッドがすべてのターゲットに再委託され、最後の呼び出しの戻り値のみが返されます。

また、色が必要な場合は、STDOUTまたはSTDERRを最後に配置する必要があります。これは、出力されるはずの色が2つしかないためです。しかし、その後、ファイルに色を出力します。

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

1

私はあなたがこれらのいくつかのことをすることを可能にする小さなRubyGemを書きました:

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

あなたはgithubでコードを見つけることができます:teerb


1

もう一つの方法。タグ付きロギングを使用していて、別のログファイルにもタグが必要な場合は、この方法で行うことができます

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

この後、代替ロガーでuuidタグを取得します

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

それが誰かを助けることを願っています。


シンプルで信頼性が高く、見事に機能します。ありがとう!注ActiveSupport::Loggerあなただけ使用する必要がある-これで箱から出して動作しますRails.logger.extendActiveSupport::Logger.broadcast(...)
XtraSimplicity

0

もう1つのオプション;-)

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

MultiIOアプローチが好きです。Ruby Loggerでうまく動作します純粋なIOを使用する場合、IOオブジェクトが持つことが期待されるいくつかのメソッドがないため、動作が停止します。パイプはここで前に言及されました:ファイルだけでなく標準出力へのルビーロガーログ出力をどのように持つことができますか?。これが私にとって最も効果的な方法です。

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

なお、私はこれが直接の質問に答えていない知っているが、それが強く関連しています。複数のIOへの出力を検索するたびに、このスレッドに出くわしたので、これも役に立つと思います。


0

これは、@ 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

これには、外部クラスラッパーを必要とせず、彼と同じ利点があります。別のrubyファイルに含めると便利なユーティリティです。

これをワンライナーとして使用して、次のようなデリゲーターインスタンスを生成します。

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

または、次のようにファクトリとして使用します。

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のLoog::Teeオブジェクトを使用できますloog

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

まさにあなたが探しているもの。


0

の使用ActiveSupportに問題がない場合は、チェックアウトすることを強くお勧めしますActiveSupport::Logger.broadcast。これは、ログの宛先をロガーに追加するための優れた非常に簡潔な方法です。

実際、Rails 4+を使用している場合(このcommit以降)、少なくともを使用している場合は、目的の動作を得るために何もする必要はありませんrails console。を使用するとrails console、Railsは自動的に拡張Rails.loggerして、通常のファイルの宛先(log/production.logたとえば)と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

いくつかの未知の不幸な理由のために、この方法は文書化されていませんが、ソースコードまたはブログの投稿を参照して、それがどのように機能するかを確認したり、例を参照したりできます。

https://www.joshmcarthur.com/til/2018/08/16/logging-to-multiple-destinations-using-activesupport-4.htmlには別の例があります:

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

私も最近この必要性があるので、これを行うライブラリを実装しました。私はこのStackOverflowの質問を見つけたので、それを必要とするすべての人のために公開します:https : //github.com/agis/multi_io

ここで説明した他のソリューションと比較して、これIOは独自のオブジェクトになるように努力しているため、他の通常のIOオブジェクト(ファイル、ソケットなど)のドロップイン代替として使用できます。

そうは言っても、私はまだすべての標準IOメソッドを実装していませんが、それらはIOセマンティクスに従ってい#writeます(たとえば、基になるすべてのIOターゲットに書き込まれたバイト数の合計を返します)。


-3

あなたのSTDOUTは重要なランタイム情報と発生したエラーに使用されていると思います。

だから私は

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

デバッグと通常のログを記録し、次にいくつかを書きました

  puts "doing stuff..."

スクリプトが実行されていたというSTDOUT情報を確認する必要がある場所です。

ええと、ちょうど私の10セント:-)

弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.