Jak mogę mieć wyjście dziennika Ruby Logger na standardowe wyjście, a także do pliku?


95

Coś w rodzaju tee w loggerze.


1
Dodanie | teezanim plik zadziałał dla mnie, więc Logger.new("| tee test.log"). Zwróć uwagę na rurę. To pochodzi z porady
Mike W

@mjwatts Użyj, tee --append test.logaby zapobiec nadpisywaniu.
fangxing

Odpowiedzi:


124

Możesz napisać pseudoklasę, IOktóra będzie zapisywać do wielu IOobiektów. Coś jak:

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

Następnie ustaw to jako plik dziennika:

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

Co czas Loggerpołączenia putsna swoim MultiIOobiekcie, będzie pisać do obu STDOUTi pliku dziennika.

Edycja: poszedłem dalej i rozgryzłem resztę interfejsu. Urządzenie rejestrujące musi odpowiadać na writei close(nie puts). MultiIOPowinno to działać, o ile odpowiada na nie i przekazuje je do rzeczywistych obiektów we / wy.


jeśli spojrzysz na ktora loggera, zobaczysz, że zakłóci to rotację logów. 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
Uwaga w Rubim 2.2 @targets.each(&:close)jest amortyzowana.
xis

Pracowało dla mnie, dopóki nie zdałem sobie sprawy, że muszę okresowo wywoływać: close na log_file, aby uzyskać plik log_file w celu zaktualizowania tego, co zarejestrował rejestrator (zasadniczo "zapisz"). STDOUT nie lubił: bliskie bycie na nim wezwanym, coś w rodzaju pokonania idei MultoIO. Dodano hack do pominięcia: blisko, z wyjątkiem klasy Plik, ale żałuję, że nie mam bardziej eleganckiego rozwiązania.
Kim Miller

48

Rozwiązanie @ Davida jest bardzo dobre. Stworzyłem ogólną klasę delegatora dla wielu celów w oparciu o jego kod.

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)

Czy mógłbyś wyjaśnić, w jaki sposób jest to lepsze lub jakie są ulepszone narzędzia tego podejścia niż proste sugerowane przez Davida
Manish Sapariya

5
To oddzielenie obaw. MultiDelegator wie tylko o delegowaniu wywołań do wielu celów. Fakt, że urządzenie rejestrujące wymaga metody zapisu i zamknięcia, jest zaimplementowany w programie wywołującym. To sprawia, że ​​MultiDelegator jest użyteczny w innych sytuacjach niż logowanie.
jonas054

Niezłe rozwiązanie. Próbowałem użyć tego do przeniesienia danych wyjściowych z moich zadań rake do pliku dziennika. Aby jednak działał z putsami (aby móc wywołać $ stdout.puts bez otrzymywania „metody prywatnej„ puts ”o nazwie”), musiałem dodać kilka innych metod: log_file = File.open ("tmp / rake.log "," a ") $ stdout = MultiDelegator.delegate (: write,: close,: puts,: print) .to (STDOUT, log_file) Byłoby miło, gdyby można było stworzyć klasę Tee, która dziedziczy z MultiDelegator, tak jak można to zrobić z klasą Delegator w stdlib ...
Tyler Rick

Wymyśliłem implementację podobną do Delegatora, którą nazwałem DelegatorToAll. W ten sposób nie musisz wymieniać wszystkich metod, które chcesz delegować, ponieważ deleguje wszystkie metody zdefiniowane w klasie delegata (IO): class Tee <DelegateToAllClass (IO) end $ stdout = Tee.new (STDOUT , File.open ("# { PLIK } .log", "a")) Więcej informacji znajdziesz na gist.github.com/TylerRick/4990898 .
Tyler Rick,

1
Naprawdę podoba mi się twoje rozwiązanie, ale nie jest dobre jako ogólny delegator, którego można używać wiele razy, ponieważ każda delegacja zanieczyszcza wszystkie wystąpienia nowymi metodami. Poniżej zamieściłem odpowiedź ( stackoverflow.com/a/36659911/123376 ), która rozwiązuje ten problem. Opublikowałem odpowiedź, a nie edycję, ponieważ zobaczenie różnicy między dwoma implementacjami może być pouczające, ponieważ zamieściłem również przykłady.
Rado

35

Jeśli jesteś w Railsach 3 lub 4, jak wskazuje ten wpis na blogu , Rails 4 ma wbudowaną tę funkcjonalność . Więc możesz zrobić:

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

Lub jeśli korzystasz z Rails 3, możesz to przenieść:

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

czy ma to zastosowanie poza szynami, czy tylko po szynach?
Ed Sykes,

Jest oparty na ActiveSupport, więc jeśli masz już tę zależność, możesz extenddowolną ActiveSupport::Loggerinstancję, jak pokazano powyżej.
phillbaker

Dzięki, to było pomocne.
Lucas

Myślę, że to najprostsza i najskuteczniejsza odpowiedź, chociaż miałem trochę dziwności, używając config.logger.extend()wewnętrznej konfiguracji mojego środowiska. Zamiast ustawić config.loggersię STDOUTw moim otoczeniu, a następnie rozszerzyła rejestratora w różnych inicjalizatorów.
mattsch,

14

Dla tych, którzy lubią prostotę:

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

źródło

Lub wydrukuj wiadomość w programie formatującym 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

W rzeczywistości używam tej techniki do drukowania do pliku dziennika, usługi rejestratora w chmurze (logentries), a jeśli jest to środowisko programistyczne - również do drukowania na STDOUT.


2
"| tee test.log"nadpisze stare wyjścia, może "| tee -a test.log"zamiast tego
fangxing

13

Chociaż podobają mi się inne sugestie, stwierdziłem, że mam ten sam problem, ale chciałem mieć możliwość posiadania różnych poziomów rejestrowania dla STDERR i pliku.

Skończyło się na strategii routingu, która multipleksuje na poziomie rejestratora, a nie na poziomie IO, tak aby każdy rejestrator mógł następnie działać na niezależnych poziomach dziennika:

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
Najbardziej podoba mi się to rozwiązanie, ponieważ jest (1) proste i (2) zachęca do ponownego użycia klas Loggera zamiast zakładać, że wszystko trafia do pliku. W moim przypadku chciałbym zalogować się do STDOUT i appendera GELF dla Graylog. Posiadanie MultiLoggerpolubienia, które opisuje @dsz, to świetne dopasowanie. Dzięki za udostępnienie!
Eric Kramer

Dodano sekcję dotyczącą obsługi pseudozmiennych (ustawiających / pobierających)
Eric Kramer

11

Możesz również dodać funkcję rejestrowania wielu urządzeń bezpośrednio do Loggera:

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

Na przykład:

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

Oto kolejna implementacja, zainspirowana odpowiedzią @ jonas054 .

Używa wzorca podobnego do Delegator. W ten sposób nie musisz wymieniać wszystkich metod, które chcesz delegować, ponieważ deleguje wszystkie metody zdefiniowane w dowolnym z obiektów docelowych:

class Tee < DelegateToAllClass(IO)
end

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

Powinieneś być w stanie używać tego również z Loggerem.

delegate_to_all.rb jest dostępny tutaj: https://gist.github.com/TylerRick/4990898



3

Odpowiedź @ jonas054 powyżej jest świetna, ale zanieczyszcza MultiDelegatorklasę każdym nowym delegatem. Jeśli użyjesz MultiDelegatorkilka razy, będzie nadal dodawać metody do klasy, co jest niepożądane. (Zobacz na przykład poniżej)

Oto ta sama implementacja, ale przy użyciu klas anonimowych, aby metody nie zanieczyszczały klasy delegatora.

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

Oto przykład zanieczyszczenia metody oryginalną implementacją, w przeciwieństwie do zmodyfikowanej implementacji:

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

Wszystko jest dobrze powyżej. teema writemetodę, ale nie ma sizemetody zgodnie z oczekiwaniami. Teraz zastanów się, kiedy tworzymy kolejnego delegata:

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

O nie, tee2odpowiada sizezgodnie z oczekiwaniami, ale reaguje również z writepowodu pierwszego delegata. Nawet teeteraz reaguje z sizepowodu zanieczyszczenia metody.

W porównaniu z rozwiązaniem klasy anonimowej, wszystko jest zgodne z oczekiwaniami:

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

Czy jesteś ograniczony do standardowego rejestratora?

Jeśli nie, możesz użyć 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

Jedna zaleta: możesz także zdefiniować różne poziomy logowania dla standardowego wyjścia i pliku.


1

Poszedłem do tego samego pomysłu "Delegowania wszystkich metod do elementów podrzędnych", który inni ludzie już odkryli, ale zwracam dla każdego z nich wartość zwracaną przez ostatnie wywołanie metody. Jeśli tego nie zrobiłem, zepsuło się, logger-colorsco oczekiwałem, Integera mapa zwracała 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

Spowoduje to ponowne delegowanie każdej metody do wszystkich celów i zwrócenie tylko wartości zwracanej przez ostatnie wywołanie.

Ponadto, jeśli chcesz, aby kolory, STDOUT lub STDERR muszą być umieszczone na końcu, ponieważ to jedyne dwa, na których mają być wyświetlane kolory. Ale wtedy również wyprowadzi kolory do twojego pliku.

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

1

Napisałem mały RubyGem, który pozwala ci zrobić kilka z tych rzeczy:

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

Możesz znaleźć kod na github: teerb


1

Jeszcze jeden sposób. Jeśli używasz tagowanego rejestrowania i potrzebujesz tagów również w innym pliku dziennika, możesz to zrobić w ten sposób

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

Po tym otrzymasz tagi uuid w alternatywnym loggerze

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

Mam nadzieję, że to komuś pomoże.


Prosty, niezawodny i działa genialnie. Dzięki! Zwróć uwagę, że ActiveSupport::Loggerdziała to po wyjęciu z pudełka - wystarczy użyć Rails.logger.extendz ActiveSupport::Logger.broadcast(...).
XtraSimplicity

0

Jeszcze jedna opcja ;-)

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

Podoba mi się podejście MultiIO . Działa dobrze z Ruby Logger . Jeśli używasz czystego IO , przestaje on działać, ponieważ brakuje mu niektórych metod, które powinny mieć obiekty IO. Potoki zostały tutaj wspomniane wcześniej: Jak mogę mieć wyjście dziennika rejestratora ruby ​​na standardowe wyjście, a także do pliku? . Oto, co działa najlepiej dla mnie.

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

Uwaga Wiem, że to nie odpowiada bezpośrednio na pytanie, ale jest silnie powiązane. Ilekroć szukałem danych wyjściowych do wielu operacji we / wy, natknąłem się na ten wątek, więc mam nadzieję, że to również okaże się przydatne.


0

To jest uproszczenie rozwiązania @ 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

Ma wszystkie te same zalety, co jego, bez potrzeby zewnętrznego opakowania klasy. Jest to przydatne narzędzie w osobnym pliku ruby.

Użyj go jako jednowierszowego, aby wygenerować wystąpienia delegatora, takie jak:

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

LUB użyj go jako fabryki:

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

Możesz użyć Loog::Teeobiektu z loogklejnotu:

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

Dokładnie to, czego szukasz.


0

Jeśli nie masz nic ActiveSupportprzeciwko używaniu , gorąco polecam sprawdzenie ActiveSupport::Logger.broadcast, co jest doskonałym i bardzo zwięzłym sposobem dodawania dodatkowych miejsc docelowych dziennika do rejestratora.

W rzeczywistości, jeśli używasz Rails 4+ (od tego zatwierdzenia ), nie musisz nic robić , aby uzyskać pożądane zachowanie - przynajmniej jeśli używasz rails console. Za każdym razem, gdy używasz rails console, Railsy automatycznie rozszerzają zakres Rails.loggertak, że wysyła zarówno do swojego zwykłego miejsca docelowego pliku ( log/production.logna przykład), jak i 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

Z jakiegoś nieznanego i niefortunnego powodu ta metoda jest nieudokumentowana, ale możesz odwołać się do kodu źródłowego lub postów na blogu, aby dowiedzieć się, jak to działa lub zobaczyć przykłady.

https://www.joshmcarthur.com/til/2018/08/16/logging-to-multiple-destinations-using-activesupport-4.html ma inny przykład:

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

Ostatnio też mam tę potrzebę, więc zaimplementowałem bibliotekę, która to robi. Właśnie odkryłem to pytanie StackOverflow, więc umieszczam je dla każdego, kto go potrzebuje: https://github.com/agis/multi_io .

W porównaniu z innymi wymienionymi tutaj rozwiązaniami, to stara się być IOwłasnym obiektem, więc może być używany jako zamiennik dla innych zwykłych obiektów IO (plików, gniazd itp.)

To powiedziawszy, nie zaimplementowałem jeszcze wszystkich standardowych metod IO, ale te, które są, są zgodne z semantyką IO (np. #writeZwraca sumę liczby bajtów zapisanych do wszystkich bazowych celów IO).


-3

Myślę, że twój STDOUT jest używany do krytycznych informacji o czasie wykonywania i podniesionych błędów.

Więc używam

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

rejestrować debugowanie i regularne rejestrowanie, a następnie napisałem kilka

  puts "doing stuff..."

gdzie muszę zobaczyć informację STDOUT, że moje skrypty w ogóle działały!

Tak, tylko moje 10 centów :-)

Korzystając z naszej strony potwierdzasz, że przeczytałeś(-aś) i rozumiesz nasze zasady używania plików cookie i zasady ochrony prywatności.
Licensed under cc by-sa 3.0 with attribution required.