如何禁用ActiveRecord对特定列的日志记录?

16

我遇到了一个问题,我认为这对大多数Rails用户来说肯定是个问题,但我还没有找到任何解决方案。

例如,当上传一个可能很大的二进制文件并将其存储在数据库中时,你肯定不希望Rails或ActiveRecord在开发模式下记录此特定字段(日志文件,标准输出)。在处理相当大的文件时,这会导致查询执行失败,几乎让我的终端崩溃。

有没有可靠且非hacky的方法来禁用特定字段的日志记录?请记住,我不是要禁用请求参数的日志记录 - 这已经得到了很好的解决。

感谢任何相关信息!


有一个过滤选项,可以像密码一样进行过滤,不知道是否有帮助。你会考虑覆盖 ActiveRecord 执行 hacky 的方法吗?如果不是,那么这就是你的提示。 - Sully
10个回答

6
如果有所帮助,这里是一个Rails 4.1兼容版本的代码片段,还包括非二进制绑定参数(例如文本或JSON列)的涂黑处理,并将日志增加到100个字符之前。感谢各位在此处提供的帮助!
class ActiveRecord::ConnectionAdapters::AbstractAdapter
  protected

  def log_with_binary_truncate(sql, name="SQL", binds=[], statement_name = nil, &block)
    binds = binds.map do |col, data|
      if data.is_a?(String) && data.size > 100
        data = "#{data[0,10]} [REDACTED #{data.size - 20} bytes] #{data[-10,10]}"
      end
      [col, data]
    end

    sql = sql.gsub(/(?<='\\x[0-9a-f]{100})[0-9a-f]{100,}?(?=[0-9a-f]{100}')/) do |match|
      "[REDACTED #{match.size} chars]"
    end

    log_without_binary_truncate(sql, name, binds, statement_name, &block)
  end

  alias_method_chain :log, :binary_truncate
end

1
这在Rails 4.2上绝对有效,但在5.0+上无效。据我所知,在Rails 5中也不需要它,因为Rails似乎已经内置了类似的功能。 - Ritchie
2
@Ritchie 什么是内置功能? - abonec

5

注意: 适用于Rails 3,但显然不适用于4(当回答此问题时尚未发布)

在你的application.rb文件中:

config.filter_parameters << :parameter_name

这将从日志中删除该属性,并用[FILTERED]替换

过滤参数的常见用例当然是密码,但我认为它也适用于二进制文件字段。


1
这只是过滤HTTP请求参数,而不是SQL日志中的参数吗? - sudoremo
问题不是关于mysql日志,而是关于rails日志。这种方法适用于所有的rails日志。 - agmin
6
这不会从 active record 日志中隐藏任何内容。已在 Rails 4 中进行测试。请参考 @tovodeverett 的解决方案。 - reto
@reto 这在 Rails 3 中可行,但似乎在 4 中不行。注意答案的日期 :) - agmin
4
我不认为这在Rails 3.2中适用,但很可能只是我太蠢了。 - Tim Down
显示剩余4条评论

5

在config/initializers中创建一个文件,修改ActiveRecord::ConnectionAdapters::AbstractAdapter如下:

class ActiveRecord::ConnectionAdapters::AbstractAdapter
   protected

   def log_with_trunkate(sql, name="SQL", binds=[], &block)
     b = binds.map {|k,v|
       v = v.truncate(20) if v.is_a? String and v.size > 20
       [k,v]
     }
     log_without_trunkate(sql, name, b, &block)
   end

   alias_method_chain :log, :trunkate
end

这将在输出日志中截断所有长度超过20个字符的字段。

还应该注意,这只会截断插入请求。如果更新操作发生,所有内容都会被放入 SQL 参数中,因此也需要进行截断。 - Patrik
在Rails 3.0.9中,在'log_without_trunkate'调用中参数数量错误(应为2个,实际传入了3个)。 - CHsurfer
3.0.x Rails的日志方法使用一个只接受两个参数'sql'和'name'的日志方法。因此,从参数中删除binds和块,仅从sql参数中过滤即可。 - Patrik
在Rails 4.1.4中'log_without_trunkate'调用中参数数量错误(应为1到3个参数,但实际传入了4个)。 - Colin
@Colin,请参考dbortz在Rails 4.1下的解决方案。 - Colin

3
这是@Patrik建议的方法在PostgreSQL中实现,可以适用于插入和更新。但对于其他数据库的SQL格式,正则表达式可能需要进行微调。
class ActiveRecord::ConnectionAdapters::AbstractAdapter
   protected

   def log_with_binary_truncate(sql, name="SQL", binds=[], &block)
    binds = binds.map do |col, data|
      if col.type == :binary && data.is_a?(String) && data.size > 27
        data = "#{data[0,10]}[REDACTED #{data.size - 20} bytes]#{data[-10,10]}"
      end
      [col, data]
    end

    sql = sql.gsub(/(?<='\\x[0-9a-f]{20})[0-9a-f]{20,}?(?=[0-9a-f]{20}')/) do |match|
      "[REDACTED #{match.size} chars]"
    end

    log_without_binary_truncate(sql, name, binds, &block)
   end

   alias_method_chain :log, :binary_truncate
end

我对此并不是非常满意,但目前足够好了。它保留了二进制字符串的前10个和最后10个字节,并指出中间删除了多少个字节/字符。除非被替换文本比替换文本更长(即如果没有至少20个字符要删除,则"[REDACTED xx chars]"将比替换文本更长,因此没有意义),否则它不会进行删除。我没有进行性能测试来确定在删除块时使用贪婪还是懒惰重复更快。我的直觉是选择懒惰,所以我这样做了,但如果SQL中只有一个二进制字段,那么贪婪可能会更快。

在我看来,这是这个问题中最好的解决方案。 - reto
这是mysql的正则表达式。您想将其添加到您的解决方案中吗?/(?<=x'[0-9a-f]{20})[0-9a-f]{20,}?(?=[0-9a-f]{20}')/ - reto

2
在Rails 5中,您可以将其放在初始化程序中:
module SqlLogFilter

  FILTERS = Set.new(%w(geo_data value timeline))
  def render_bind(attribute)
    return [attribute.name, '<filtered>'] if FILTERS.include?(attribute.name)
    super
  end

end
ActiveRecord::LogSubscriber.prepend SqlLogFilter

例如,对于筛选器属性geo_datavaluetimeline


1
这是一个关于Rails 5的版本。Rails 5默认情况下截断二进制数据,但不会截断长文本列。
module LogTruncater
  def render_bind(attribute)
    num_chars = Integer(ENV['ACTIVERECORD_SQL_LOG_MAX_VALUE']) rescue 120
    half_num_chars = num_chars / 2
    value = if attribute.type.binary? && attribute.value
      if attribute.value.is_a?(Hash)
        "<#{attribute.value_for_database.to_s.bytesize} bytes of binary data>"
      else
        "<#{attribute.value.bytesize} bytes of binary data>"
      end
    else
      attribute.value_for_database
    end

    if value.is_a?(String) && value.size > num_chars
      value = "#{value[0,half_num_chars]} [REDACTED #{value.size - num_chars} chars] #{value[-half_num_chars,half_num_chars]}"
    end

    [attribute.name, value]
  end

end

class ActiveRecord::LogSubscriber
  prepend LogTruncater
end

1
Rails 5.2已经发生了变化,它需要2个参数。但是根据文档,render_bind方法似乎已经完全消失了... - Hugo Logmans

0

我也没找到太多相关信息,但你可以尝试做一件事情:

ActiveRecord::Base.logger = nil

禁用日志可能不是最好的解决方案,但你可以设置ActiveRecord日志记录器为一些自定义子类,使其不记录超过特定大小的消息或执行一些更智能的操作,以解析出消息中的特定部分。

虽然这似乎不是理想的解决方案,但它看起来是可行的,尽管我还没有研究具体的实现细节。我很想听听是否有更好的解决方案。


是的,这是我想到的唯一可能性,但我认为这个解决方案相当丑陋(正如你所提到的),因为你肯定希望呼叫被记录但不包括此字段。如果没有“完美”的答案,我可能会继续实现这样一个自定义记录器来解析日志语句...同样很丑陋。 - sudoremo
看起来你想要重写Logger的“add”方法,子类化版本可以进行解析,然后调用超类的add方法,传递解析后的消息和其他参数。 - Scott S

0

适用于Rails 5.2及以上版本

module LogTruncater
  def render_bind(attr, value)
    num_chars = Integer(ENV['ACTIVERECORD_SQL_LOG_MAX_VALUE']) rescue 120
    half_num_chars = num_chars / 2

    if attr.is_a?(Array)
      attr = attr.first
    elsif attr.type.binary? && attr.value
      value = "<#{attr.value_for_database.to_s.bytesize} bytes of binary data>"
    end

    if value.is_a?(String) && value.size > num_chars
      value = "#{value[0,half_num_chars]} [REDACTED #{value.size - num_chars} chars] #{value[-half_num_chars,half_num_chars]}"
    end

    [attr && attr.name, value]
  end

end

class ActiveRecord::LogSubscriber
  prepend LogTruncater
end

0
我遇到了同样的问题,但是我无法找到一个干净的解决方案。最终,我编写了一个自定义格式化程序,用于过滤掉 Rails 日志中的 blob。
上面的代码需要放置在 config/initializers 中,并将 file_dat 替换为您想要删除的列,将 file_name 替换为正则表达式中出现在其后的列。

0

这是我在Rails 6中使用的方法:

# initializers/scrub_logs.rb

module ActiveSupport
  module TaggedLogging
    module Formatter # :nodoc:
      # Hide PlaygroundTemplate#yaml column from SQL queries because it's huge.
      def scrub_yaml_source(input)
        input.gsub(/\["yaml", ".*, \["/, '["yaml", "REDACTED"], ["')
      end

      alias orig_call call

      def call(severity, timestamp, progname, msg)
        orig_call(severity, timestamp, progname, scrub_yaml_source(msg))
      end
    end
  end
end

yaml替换为您的列名。


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接