Permalink
Browse files

Log filtering (#225)

  • Loading branch information...
TiteiKo authored and jodosha committed Aug 11, 2017
1 parent 3df75c5 commit 651cfe8b4153dc77ce95b3982e079d89bec95914
Showing with 165 additions and 12 deletions.
  1. +73 −12 lib/hanami/logger.rb
  2. +92 −0 spec/unit/hanami/logger_spec.rb
@@ -3,6 +3,7 @@
require 'logger'
require 'hanami/utils/string'
require 'hanami/utils/json'
require 'hanami/utils/hash'
require 'hanami/utils/class_attribute'

module Hanami
@@ -117,15 +118,13 @@ class Formatter < ::Logger::Formatter
class_attribute :subclasses
self.subclasses = Set.new

def self.fabricate(formatter, application_name)
case formatter
when Symbol
(subclasses.find { |s| s.eligible?(formatter) } || self).new
when nil
new
else
formatter
end.tap { |f| f.application_name = application_name }
def self.fabricate(formatter, application_name, filters)
fabricated_formatter = _formatter_instance(formatter)

fabricated_formatter.application_name = application_name
fabricated_formatter.hash_filter = HashFilter.new(filters)

fabricated_formatter
end

# @api private
@@ -139,6 +138,20 @@ def self.eligible?(name)
name == :default
end

# @api private
# @since x.x.x
def self._formatter_instance(formatter)
case formatter
when Symbol
(subclasses.find { |s| s.eligible?(formatter) } || self).new
when nil
new
else
formatter
end
end
private_class_method :_formatter_instance

# @since 0.5.0
# @api private
attr_writer :application_name
@@ -147,6 +160,10 @@ def self.eligible?(name)
# @api private
attr_reader :application_name

# @since x.x.x
# @api private
attr_writer :hash_filter

# @since 0.5.0
# @api private
#
@@ -168,7 +185,7 @@ def call(severity, time, _progname, msg)
def _message_hash(message) # rubocop:disable Metrics/MethodLength
case message
when Hash
message
@hash_filter.filter(message)
when Exception
Hash[
message: message.message,
@@ -206,6 +223,50 @@ def _format_error(result, hash)

result
end

# Filtering logic
class HashFilter
attr_reader :filters

def initialize(filters = [])
@filters = filters
end

def filter(hash)
_filtered_keys(hash).each do |key|
*keys, last = _actual_keys(hash, key.split('.'))
keys.inject(hash, :fetch)[last] = '[FILTERED]'
end

hash
end

private

def _filtered_keys(hash)
_key_paths(hash).select { |key| filters.any? { |filter| key =~ %r{(\.|\A)#{filter}(\.|\z)} } }
end

def _key_paths(hash, base = nil)
hash.inject([]) do |results, (k, v)|
results + (v.respond_to?(:each) ? _key_paths(v, _build_path(base, k)) : [_build_path(base, k)])
end
end

def _build_path(base, key)
[base, key.to_s].compact.join('.')
end

def _actual_keys(hash, keys)
search_in = hash

keys.inject([]) do |res, key|
correct_key = search_in.key?(key.to_sym) ? key.to_sym : key
search_in = search_in[correct_key]
res + [correct_key]
end
end
end
end

# Hanami::Logger JSON formatter.
@@ -375,13 +436,13 @@ def _format(hash)
# logger.info "Hello World"
#
# # => {"app":"Hanami","severity":"DEBUG","time":"2017-03-30T13:57:59Z","message":"Hello World"}
def initialize(application_name = nil, *args, stream: $stdout, level: DEBUG, formatter: nil)
def initialize(application_name = nil, *args, stream: $stdout, level: DEBUG, formatter: nil, filter: []) # rubocop:disable Metrics/ParameterLists
super(stream, *args)

@level = _level(level)
@stream = stream
@application_name = application_name
@formatter = Formatter.fabricate(formatter, self.application_name)
@formatter = Formatter.fabricate(formatter, self.application_name, filter)
end

# Returns the current application name, this is used for tagging purposes
@@ -433,6 +433,98 @@ class TestLogger < Hanami::Logger; end
end
expect(output).to eq "[hanami] [INFO] [2017-01-15 16:00:23 +0100] foo bar\n"
end

it 'displays filtered hash values' do
form_params = Hash[
form_params: Hash[
'name' => 'John',
'password' => '[FILTERED]',
'password_confirmation' => '[FILTERED]'
]
]

expected = "{\"name\"=>\"John\", \"password\"=>\"[FILTERED]\", \"password_confirmation\"=>\"[FILTERED]\"}"

output = with_captured_stdout do
class TestLogger < Hanami::Logger; end
TestLogger.new.info(form_params)
end

expect(output).to eq("[hanami] [INFO] [2017-01-15 16:00:23 +0100] #{expected}\n")
end
end

context do
let(:form_params) do
Hash[
form_params: Hash[
'password' => 'password',
'password_confirmation' => 'password',
'credit_card' => Hash[
'number' => '4545 4545 4545 4545',
'name' => 'John Citizen'
],
'user' => Hash[
'login' => 'John',
'name' => 'John'
]
]
]
end

describe 'with filters' do
it 'filters values for keys in the filters array' do
expected = %s({"password"=>"[FILTERED]", "password_confirmation"=>"[FILTERED]", "credit_card"=>{"number"=>"[FILTERED]", "name"=>"[FILTERED]"}, "user"=>{"login"=>"[FILTERED]", "name"=>"John"}})

output = with_captured_stdout do
class TestLogger < Hanami::Logger; end
filters = %w(password password_confirmation credit_card user.login)
TestLogger.new(filter: filters).info(form_params)
end

expect(output).to eq("[hanami] [INFO] [2017-01-15 16:00:23 +0100] #{expected}\n")
end
end

describe 'without filters' do
it 'outputs unfiltered params' do
expected = %s({"password"=>"password", "password_confirmation"=>"password", "credit_card"=>{"number"=>"4545 4545 4545 4545", "name"=>"John Citizen"}, "user"=>{"login"=>"John", "name"=>"John"}})

output = with_captured_stdout do
class TestLogger < Hanami::Logger; end
TestLogger.new.info(form_params)
end

expect(output).to eq("[hanami] [INFO] [2017-01-15 16:00:23 +0100] #{expected}\n")
end
end
end
end

describe Hanami::Logger::Formatter::HashFilter do
context 'without filters' do
it "doesn't filter" do
input = Hash[password: 'azerty']
output = described_class.new.filter(input)
expect(output).to eql(input)
end
end

it "doesn't alter the hash keys" do
output = described_class.new(%w(password)).filter(Hash["password" => 'azerty', foo: Hash[password: 'bar']])
expect(output).to eql(Hash["password" => '[FILTERED]', foo: Hash[password: '[FILTERED]']])
end

it 'filters with multiple filters' do
input = Hash[password: 'azerty', number: '12345']
output = described_class.new(%i(password number)).filter(input)
expect(output).to eql(Hash[password: '[FILTERED]', number: '[FILTERED]'])
end

it 'filters with multi-level filter' do
input = Hash[user: Hash[name: 'foo', password: 'azerty'], password: 'foo']
output = described_class.new(%w(user.password)).filter(input)
expect(output).to eql(Hash[user: Hash[name: 'foo', password: '[FILTERED]'], password: 'foo'])
end
end
end

0 comments on commit 651cfe8

Please sign in to comment.