Как я могу вывести журнал журнала Ruby на стандартный вывод, а также в файл?

95

Что-то вроде функциональности тройника в регистраторе.

Маниш Сапария
источник
1
Добавление | teeдо того, как файл сработал для меня, поэтому Logger.new("| tee test.log"). Обратите внимание на трубу. Это было из подсказки на coderwall.com/p/y_b3ra/…
Майк В.
@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 регистратора, вы увидите, что это испортит ротацию журнала. 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
Работал для меня, пока я не понял, что мне нужно периодически вызывать: close в 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)
jonas054
источник
Не могли бы вы объяснить, чем это лучше или какие усовершенствованные возможности этого подхода, чем простой, предложенный Давидом
Маниш Сапария
5
Это разделение забот. MultiDelegator знает только о делегировании вызовов нескольким целям. Тот факт, что устройству регистрации требуется метод записи и закрытия, реализован в вызывающей программе. Это делает MultiDelegator пригодным для использования в других ситуациях, кроме ведения журнала.
jonas054
Хорошее решение. Я попытался использовать это, чтобы записать вывод моих задач с граблями в файл журнала. Однако для того, чтобы заставить его работать с путями (чтобы иметь возможность вызывать $ stdout.puts без "вызова частного метода" put '), мне пришлось добавить еще несколько методов: log_file = File.open ("tmp / rake.log "," a ") $ stdout = MultiDelegator.delegate (: write,: close,: put,: print) .to (STDOUT, log_file) Было бы неплохо, если бы можно было создать класс Tee, унаследованный от MultiDelegator, как вы можете сделать с классом Delegator в stdlib ...
Тайлер Рик
Я придумал реализацию этого типа Delegator, которую назвал 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 ), который устраняет эту проблему. Я опубликовал ответ, а не редактирование, так как может быть полезно увидеть разницу между двумя реализациями, поскольку я также опубликовал примеры.
Rado
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 07
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

На самом деле я использую этот метод для печати в файл журнала, службу облачного журнала (logentries) и, если это среда разработки, также печать в STDOUT.

Игорь
источник
2
"| tee test.log"перезапишет старые выходные данные, может быть "| tee -a test.log"вместо этого
fangxing
13

Хотя мне очень нравятся другие предложения, я обнаружил, что у меня такая же проблема, но мне нужна возможность иметь разные уровни ведения журнала для STDERR и файла.

В итоге я выбрал стратегию маршрутизации, которая мультиплексируется на уровне регистратора, а не на уровне ввода-вывода, так что каждый регистратор может работать на независимых уровнях журнала:

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)
dsz
источник
1
Мне больше всего нравится это решение, так как оно (1) простое и (2) побуждает вас повторно использовать ваши классы Logger вместо того, чтобы предполагать, что все идет в файл. В моем случае я хотел бы войти в STDOUT и приложение GELF для Graylog. Имея MultiLoggerкак @dsz описывает это отлично подходит. Спасибо, что поделился!
Эрик Крамер
Добавлен раздел для обработки псевдопеременных (сеттеры / геттеры)
Эрик Крамер
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"))

Вы также сможете использовать это с 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
Rado
источник
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

Я пошел к той же идее «Делегирование всех методов подэлементам», которую уже исследовали другие люди, но я возвращаю для каждого из них возвращаемое значение последнего вызова метода. Если я этого не сделал, он сломался, 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"
Jerska
источник
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

Надеюсь, это кому-то поможет.

Retgoat
источник
Просто, надежно и безупречно работает. Благодарность! Обратите внимание, что это 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 . Если вы используете чистый ввод-вывод, он перестает работать, потому что ему не хватает некоторых методов, которые должны быть у объектов ввода-вывода. Каналы были упомянуты ранее здесь: Как я могу вывести журнал журнала Ruby на стандартный вывод, а также в файл? . Вот что мне больше всего подходит.

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

Обратите внимание: я знаю, что это не дает прямого ответа на вопрос, но это тесно связано. Всякий раз, когда я искал вывод для нескольких операций ввода-вывода, я сталкивался с этой веткой, так что надеюсь, вы тоже найдете это полезным.

Knugie
источник
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

Он имеет все те же преимущества, что и его, без необходимости во внешней оболочке класса. Это полезная утилита, хранящаяся в отдельном рубиновом файле.

Используйте его как однострочник для создания экземпляров делегатора, например:

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объект из loogкамня:

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

Именно то, что вы ищете.

Егор256
источник
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возвращают сумму количества байтов, записанных для всех основных целей ввода-вывода).

Agis
источник
-3

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

Поэтому я использую

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

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

  puts "doing stuff..."

где мне нужно увидеть информацию STDOUT о том, что мои скрипты вообще выполнялись!

Ба, только мои 10 центов :-)

rupweb
источник