Журнал ActiveRecord, какой запрос отправился в базу данных

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

Base1.connection.select_value("select * from foo")
Base2.connection.select_value("select * from foo")

Выполните следующие записи журнала:

D, [2017-03-13T09:27:11.844395 #22112] DEBUG -- :    (0.6ms)  select * from foo
D, [2017-03-13T09:27:11.844539 #22112] DEBUG -- :    (0.1ms)  select * from foo

Как я могу заставить журнал базы данных ActiveRecord указать, в какой базе данных был выполнен запрос?

Автономный пример

begin
  require "bundler/inline"
rescue LoadError => e
  $stderr.puts "Bundler version 1.10 or later is required."
  raise e
end

gemfile(true) do
  source "https://rubygems.org"
  # Activate the gem you are reporting the issue against.
  gem "activerecord", "4.2.8"
  gem "sqlite3"
end

require "active_record"
require "logger"

class Base1 < ActiveRecord::Base
  self.abstract_class = true
end

class Base2 < ActiveRecord::Base
  self.abstract_class = true
end

Base1.establish_connection(adapter: "sqlite3", database: ":memory:")
Base2.establish_connection(adapter: "sqlite3", database: ":memory:")

ActiveRecord::Base.logger = Logger.new(STDOUT)

Base1.connection.execute("create table foo(i int)")
Base2.connection.execute("create table foo(i int)")
Base1.connection.execute("insert into foo(i) values (1)")
Base2.connection.execute("insert into foo(i) values (2)")
raise unless Base1.connection.select_value("select * from foo") == 1
raise unless Base2.connection.select_value("select * from foo") == 2

Выход:

D, [2017-03-13T09:27:11.842939 #22112] DEBUG -- :    (0.2ms)  create table foo(i int)
D, [2017-03-13T09:27:11.843478 #22112] DEBUG -- :    (0.2ms)  create table foo(i int)
D, [2017-03-13T09:27:11.843612 #22112] DEBUG -- :    (0.1ms)  insert into foo(i) values (1)
D, [2017-03-13T09:27:11.843720 #22112] DEBUG -- :    (0.0ms)  insert into foo(i) values (2)
D, [2017-03-13T09:27:11.844395 #22112] DEBUG -- :    (0.6ms)  select * from foo
D, [2017-03-13T09:27:11.844539 #22112] DEBUG -- :    (0.1ms)  select * from foo

Я попытался создать отдельный регистратор для каждого соединения

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

Base1.logger = Logger.new(STDOUT)
Base2.logger = Logger.new(STDOUT)

Но, к сожалению, в ActiveRecord есть только один регистратор, как показано в этой строке, которая не вызывает исключения:

raise unless Base1.logger.object_id == Base2.logger.object_id

Версия

  • рубиново-2.3.3
  • activerecord 4.2.8
  • В этом примере sqlite3 1.13.3 (1)
  • В процессе производства mysql2 0.4.5 (1) и activerecord-sqlserver-adapter 4.2.15 (1)

(1) Эта проблема не специфична для какого-либо конкретного адаптера базы данных. Я перечислил версии адаптеров для полноты.

Ответы

Ответ 1

Как я знаю, вы не можете сделать это с ActiveRecord изначально. Но вы можете переопределить метод log в AbstractAdapter, если он вам действительно нужен:

class ActiveRecord::ConnectionAdapters::AbstractAdapter
  alias :original_log :log
  def log(sql, name = "SQL", binds = [], statement_name = nil)
    #add info that you want to display to name
    name = "#{name} #{@connection.hash}" 
    original_log(sql, name, binds, statement_name) { yield }
  end
end

Выход:

D, [2017-03-15T20:55:59.200533 #73440] DEBUG -- :    -4111614587995646180 (0.5ms)  create table foo(i int)
D, [2017-03-15T20:55:59.201178 #73440] DEBUG -- :    -4097137311320758185 (0.1ms)  create table foo(i int)
D, [2017-03-15T20:55:59.201298 #73440] DEBUG -- :    -4111614587995646180 (0.0ms)  insert into foo(i) values (1)
D, [2017-03-15T20:55:59.201426 #73440] DEBUG -- :    -4097137311320758185 (0.1ms)  insert into foo(i) values (2)
D, [2017-03-15T20:55:59.202229 #73440] DEBUG -- :    -4111614587995646180 (0.7ms)  select * from foo
D, [2017-03-15T20:55:59.202360 #73440] DEBUG -- :    -4097137311320758185 (0.0ms)  select * from foo

Быть более удобным для людей

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

Что мы можем сделать, так это украсить абстрактный адаптер с помощью метода, чтобы вернуть удобное для пользователя имя соединения. Во время инициализации вашей программы добавьте метод #log_name к каждому адаптеру подключения:

Base1.connection.define_singleton_method(:log_name) { "one" }
Base2.connection.define_singleton_method(:log_name) { "two" }

Теперь обезьяна-патч может использовать метод #log_name:

class ActiveRecord::ConnectionAdapters::AbstractAdapter
  alias :original_log :log
  def log(sql, name = "SQL", binds = [], statement_name = nil)
    connection_name = respond_to?(:log_name) ? log_name : nil
    name = [connection_name, name].compact.join(" ")
    original_log(sql, name, binds, statement_name) { yield }
  end
end

Выход:

D, [2017-03-21T10:10:53.330021 #22147] DEBUG -- :   one (0.3ms)  create table foo(i int)
D, [2017-03-21T10:10:53.330380 #22147] DEBUG -- :   two (0.2ms)  create table foo(i int)
D, [2017-03-21T10:10:53.330464 #22147] DEBUG -- :   one (0.0ms)  insert into foo(i) values (1)
D, [2017-03-21T10:10:53.330536 #22147] DEBUG -- :   two (0.0ms)  insert into foo(i) values (2)
D, [2017-03-21T10:10:53.331002 #22147] DEBUG -- :   one (0.4ms)  select * from foo
D, [2017-03-21T10:10:53.331104 #22147] DEBUG -- :   two (0.0ms)  select * from foo

Ответ 2

Ответ @idej хорош, но я хочу также добавить еще один подход. Если это так, что вы также используете ActiveSupport, вы можете подписаться на событие sql.active_record.

ActiveSupport::Notifications.subscribe("sql.active_record") do |*args|
  event = ActiveSupport::Notifications::Event.new(*args)

  puts event.inspect
end

В event.payload у вас есть доступ к некоторым полезным атрибутам, таким как sql, connection_id, binds и т.д. Проблема заключается в том, что вам нужно самостоятельно регистрировать эти атрибуты, а сообщение для любого SQL-запроса будет отображаться два раза в журналы. Но вам не нужно изменять какой-либо класс.

Подробнее вы можете найти в docs.

Ответ 3

У вас должен (patch) патч ActiveRecord::ConnectionAdapters::AbstractAdapter вы можете сделать это в новом инициализаторе:

# config/initializers/adapter_monkeypatch.rb
class ActiveRecord::ConnectionAdapters::AbstractAdapter
  alias :original_log :log
  def log(sql, *args, &block)
    prefix = self.class.to_s.demodulize

    original_log("#{prefix}: #{sql}", *args, &block)
  end
end

Поскольку вы используете разные базы данных, вы можете просто использовать имя_файла адаптера в качестве префикса:

исправленный рельс logger Но легко изменить префикс к чему-то более соответствующему вашим требованиям.