Як я можу отримати вихід журналу журналу ruby ​​у файл stdout, а також файл?


94

Щось схоже на функціональність трійника в реєстраторі.


1
Додавання | teeдо того, як файл працював у мене, так Logger.new("| tee test.log"). Зверніть увагу на трубу. Це було з підказки на coderwall.com/p/y_b3ra/…
Mike W

@mjwatts Використовується tee --append test.logдля запобігання перезапису.
fangxing

Відповіді:


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ваш файл журналу.

Редагувати: Я пішов далі і зрозумів решту інтерфейсу. Пристрій реєстрації повинно реагувати на ( writeі closeне puts). Поки MultiIOреагує на них і проксіє їх до реальних об'єктів вводу-виводу, це повинно працювати.


якщо ви подивитесь на ctor logger, то побачите, що це зіпсує обертання журналу. 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 @targets.each(&:close)знецінена.
xis

Працював у мене, поки я не зрозумів, що мені потрібно періодично телефонувати: закрити log_file, щоб отримати log_file, щоб оновити те, що реєстрував журнал (по суті, "зберегти"). STDOUT не сподобався: близький заклик до нього, свого роду перемога ідеї MultoIO. Додано хак для пропуску: закрити, за винятком класу File, але хотів би, щоб у мене було більш елегантне рішення.
Кім Міллер,

48

@ Рішення Девіда дуже хороше. Я створив загальний клас делегатора для декількох цілей на основі його коду.

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)

Не могли б ви пояснити, наскільки це краще або які покращені утиліти цього підходу, ніж простий, запропонований Девідом
Маніш Сапарія,

5
Це розділення проблем. MultiDelegator знає лише про делегування викликів кільком цілям. Той факт, що пристрій реєстрації потребує методу запису та закриття, реалізований у абонента. Це робить MultiDelegator придатним для використання в інших ситуаціях, крім реєстрації.
jonas054

Гарне рішення. Я спробував використати це, щоб зв'язати вихідні дані моїх рейкових завдань до файлу журналу. Для того, щоб змусити його працювати з путами (щоб мати можливість викликати $ stdout.puts, не отримуючи "приватний метод` путів ', що називається "), мені довелося додати ще кілька методів: log_file = File.open (" tmp / rake.log "," a ") $ stdout = MultiDelegator.delegate (: write,: close,: put,: print) .to (STDOUT, log_file) Було б непогано, якби можна було створити клас Tee, який успадкував від MultiDelegator, як це можна зробити з класом Delegator у stdlib ...
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 ), яка вирішує цю проблему. Я розмістив відповідь, а не редагування, оскільки може бути освітнім побачити різницю між двома реалізаціями, оскільки я також розмістив приклади.
Радо

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

це застосовне поза рейок чи лише рейок?
Ед Сайкс,

Він базується на ActiveSupport, тому, якщо у вас вже є така залежність, ви можете extendбудь-який ActiveSupport::Loggerекземпляр, як показано вище.
phillbaker

Дякую, це було корисно.
Лукас

Я думаю, що це найпростіша та найефективніша відповідь, хоча я мав певні дивацтва, використовуючи config.logger.extend()внутрішню конфігурацію мого середовища. Натомість я встановив config.loggerзначення STDOUTв моєму середовищі, а потім розширив реєстратор в різні ініціалізатори.
mattsch

14

Для тих, хто любить це просто:

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

джерело

Або надрукуйте повідомлення у форматері 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

Я фактично використовую цю техніку для друку у файл журналу, службу хмарного реєстратора (журнали), а якщо це середовище розробника - також друкую в STDOUT.


2
"| tee test.log"воля перезаписує старі результати, можливо, "| tee -a test.log"замість цього
фанксинг

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, замість того, щоб припускати, що все надходить у файл. У моєму випадку я хотів би увійти в STDOUT та GELF додаток для Graylog. Наявність MultiLoggerподібного @dsz описує чудове рішення. Дякую, що поділились!
Ерік Крамер,

Доданий розділ для обробки
Ерік Крамер

11

Ви також можете додати декілька функцій реєстрації пристрою безпосередньо в 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

Наприклад:

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

Ви також зможете використовувати це з Logger.

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

Одна перевага: Ви також можете визначити різні рівні журналу для stdout та файлу.


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 потрібно поставити останніми, оскільки це єдині два кольори, які передбачається виводити. Але потім він також виведе кольори у ваш файл.

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.extendз ActiveSupport::Logger.broadcast(...).
XtraSimplicity

0

Ще один варіант ;-)

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. Труби були згадані раніше тут: Як я можу отримати вихід журналу журналу ruby ​​в stdout, а також у файл? . Ось що мені найкраще підходить.

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

Примітка. Я знаю, що це не відповідає безпосередньо на запитання, але воно сильно пов’язане. Щоразу, коли я шукав вихідні дані для декількох операцій вводу-виводу, я стикався з цією темою, тому, сподіваюся, це теж вам стане в нагоді.


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

Ви можете використовувати Loog::Teeоб'єкт від looggem:

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

Саме те, що ви шукаєте.


0

Якщо у вас все гаразд із використанням ActiveSupport, то настійно рекомендую перевірити ActiveSupport::Logger.broadcast, що є чудовим і дуже стислим способом додати додаткові адреси журналу до реєстратора.

Насправді, якщо ви використовуєте Rails 4+ (станом на цей коміт ), вам не потрібно нічого робити, щоб отримати бажану поведінку - принаймні, якщо ви використовуєте 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власним об'єктом, тому його можна використовувати як замінник для інших звичайних об'єктів вводу-виводу (файлів, сокетів тощо)

Тим не менш, я ще не реалізував усі стандартні методи вводу-виводу, але ті, що є, дотримуються семантики вводу-виводу (наприклад, #writeповертає суму кількості байтів, записаних до всіх основних цілей вводу-виводу).


-3

Я думаю, що ваш STDOUT використовується для отримання критичної інформації про час роботи та виявлених помилок.

Тому я використовую

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

реєструвати налагодження та регулярні журнали, а потім написав кілька

  puts "doing stuff..."

де мені потрібно побачити інформацію STDOUT про те, що мої сценарії взагалі працювали!

Бах, лише мої 10 центів :-)

Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.