logasm 0.2.6 → 0.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: 1c37a9355e4d9426215d500f6fdb50a22954c2e4
4
- data.tar.gz: 58289a6f19d649b8c405638428a43938ca586132
3
+ metadata.gz: 440665fe491c115f43c411822de433ead259e193
4
+ data.tar.gz: 06568509dacd084a9607ac545b3cfd03447947db
5
5
  SHA512:
6
- metadata.gz: 6712eee9ed98800f0be30370509848b0c8a70b587e278fd63c7e742d5949aa0c3703b70cb17bc2c8a5d9d3cb02ab7e548c1d9a06c65a5f31711d3628edb55f7e
7
- data.tar.gz: e60591d1432b92075757b0fb76acbd7613f3f46fb6ec60af58bb2bd0e54331a90f73e301b3338f8f3667f4feabc547821a8bbf99c613aac288f6193e4309cbff
6
+ metadata.gz: 380b1118c988bbf141b9c03a63ca3c68d33f1f8873709bd92d5e52188dfb3adf980edf7d040eca62c2bdbc66fb4a142e0906e2272aa278cceb4109d6c88e52a2
7
+ data.tar.gz: b93a280171c1123d5559ac630a5db52710cd429a7f5bbaddbb73885f7aa75d287d21b117ca3a8fe75b6a2399667c05bcb1631ce889795f2b493036f664742992
data/README.md CHANGED
@@ -61,3 +61,74 @@ logasm = Logasm.build('myApp', { stdout: nil, logstash: { host: "localhost", por
61
61
  ```
62
62
 
63
63
  When no loggers are specified, it creates a stdout logger by default.
64
+
65
+ ## Preprocessors
66
+
67
+ Preprocessors allow modification of log messages, prior to sending of the message to the configured logger(s).
68
+
69
+ ### Blacklist
70
+
71
+ Excludes or masks defined fields of the passed hash object.
72
+ You can specify the name of the field and which action to take on it.
73
+ Nested hashes of any level are preprocessed as well.
74
+
75
+ Available actions:
76
+
77
+ * exclude(`default`) - fully excludes the field and its value from the hash.
78
+ * mask - replaces every character from the original value with `*`. In case of `array`, `hash` or `boolean` value is replaced with one `*`.
79
+
80
+ #### Configuration
81
+
82
+ ```yaml
83
+ preprocessors:
84
+ blacklist:
85
+ fields:
86
+ - key: password
87
+ - key: phone
88
+ action: mask
89
+ ```
90
+
91
+ #### Usage
92
+
93
+ ```ruby
94
+ logger = Logasm.build(application_name, logger_config, preprocessors)
95
+
96
+ input = {password: 'password', info: {phone: '+12055555555'}}
97
+
98
+ logger.debug("Received request", input)
99
+ ```
100
+
101
+ Logger output:
102
+
103
+ ```
104
+ Received request {"info":{"phone":"************"}}
105
+ ```
106
+
107
+ ### Whitelist
108
+
109
+ Masks all the fields except those whitelisted in the configuration using [JSON Pointer](https://tools.ietf.org/html/rfc6901).
110
+ Only simple values(`string`, `number`, `boolean`) can be whitelisted.
111
+
112
+ #### Configuration
113
+
114
+ ```yaml
115
+ preprocessors:
116
+ whitelist:
117
+ pointers: ['/info/phone']
118
+ ```
119
+
120
+ #### Usage
121
+
122
+ ```ruby
123
+ logger = Logasm.build(application_name, logger_config, preprocessors)
124
+
125
+ input = {password: 'password', info: {phone: '+12055555555'}}
126
+
127
+ logger.debug("Received request", input)
128
+ ```
129
+
130
+ Logger output:
131
+
132
+ ```
133
+ Received request {password: "********", "info":{"phone":"+12055555555"}}
134
+ ```
@@ -0,0 +1,67 @@
1
+ class Logasm
2
+ module Preprocessors
3
+ class Blacklist
4
+
5
+ DEFAULT_ACTION = 'exclude'
6
+ MASK_SYMBOL = '*'
7
+
8
+ class UnsupportedActionException < Exception
9
+ end
10
+
11
+ def initialize(config = {})
12
+ @fields_to_process = config[:fields].inject({}) do |mem, field|
13
+ key = field.delete(:key)
14
+ options = {action: DEFAULT_ACTION}.merge(field)
15
+ validate_action_supported(options[:action])
16
+ mem.merge(key => options)
17
+ end
18
+ end
19
+
20
+ def process(data)
21
+ if data.is_a? Hash
22
+ data.inject({}) do |mem, (key, val)|
23
+ if (field = @fields_to_process[key.to_s])
24
+ self.send(action_method(field[:action]), mem, key, val)
25
+ else
26
+ mem.merge(key => process(val))
27
+ end
28
+ end
29
+ elsif data.is_a? Array
30
+ data.inject([]) do |mem, val|
31
+ mem + [process(val)]
32
+ end
33
+ else
34
+ data
35
+ end
36
+ end
37
+
38
+ private
39
+
40
+ def action_method(action)
41
+ "#{action}_field"
42
+ end
43
+
44
+ def validate_action_supported(action)
45
+ unless self.respond_to?(action_method(action).to_sym, true)
46
+ raise UnsupportedActionException.new("Action: #{action} is not supported")
47
+ end
48
+ end
49
+
50
+ def mask_field(data, key, val)
51
+ if val.is_a?(Hash) || val.is_a?(Array) || is_boolean?(val)
52
+ data.merge(key => MASK_SYMBOL)
53
+ else
54
+ data.merge(key => MASK_SYMBOL * val.to_s.length)
55
+ end
56
+ end
57
+
58
+ def exclude_field(data, *)
59
+ data
60
+ end
61
+
62
+ def is_boolean?(val)
63
+ val.is_a?(TrueClass) || val.is_a?(FalseClass)
64
+ end
65
+ end
66
+ end
67
+ end
@@ -0,0 +1,88 @@
1
+ class Logasm
2
+ module Preprocessors
3
+ class Whitelist
4
+
5
+ DEFAULT_WHITELIST = %w(/id /message /correlation_id /queue)
6
+ MASK_SYMBOL = '*'
7
+
8
+ class InvalidPointerFormatException < Exception
9
+ end
10
+
11
+ def initialize(config = {})
12
+ pointers = (config[:pointers] || []) + DEFAULT_WHITELIST
13
+ @fields_to_include = pointers.inject({}) do |mem, pointer|
14
+ validate_pointer(pointer)
15
+ mem.merge(decode(pointer) => true)
16
+ end
17
+ end
18
+
19
+ def process(data)
20
+ process_data('', data)
21
+ end
22
+
23
+ private
24
+
25
+ def validate_pointer(pointer)
26
+ if pointer.slice(-1) == '/'
27
+ raise InvalidPointerFormatException.new('Pointer should not contain trailing slash')
28
+ end
29
+ end
30
+
31
+ def decode(pointer)
32
+ pointer
33
+ .gsub('~0', '~')
34
+ .gsub('~1', '/')
35
+ end
36
+
37
+ def process_data(parent_pointer, data)
38
+ self.send("process_#{get_type(data)}", parent_pointer, data)
39
+ end
40
+
41
+ def get_type(data)
42
+ if data.is_a? Hash
43
+ 'hash'
44
+ elsif data.is_a? Array
45
+ 'array'
46
+ else
47
+ 'value'
48
+ end
49
+ end
50
+
51
+ def process_hash(parent_pointer, hash)
52
+ hash.inject({}) do |mem, (key, value)|
53
+ pointer = "#{parent_pointer}/#{key}"
54
+ processed_value = process_data(pointer, value)
55
+ mem.merge(key => processed_value)
56
+ end
57
+ end
58
+
59
+ def process_array(parent_pointer, array)
60
+ array.each_with_index.inject([]) do |mem, (value, index)|
61
+ pointer = "#{parent_pointer}/#{index}"
62
+ processed_value = process_data(pointer, value)
63
+ mem + [processed_value]
64
+ end
65
+ end
66
+
67
+ def process_value(parent_pointer, value)
68
+ if @fields_to_include[parent_pointer]
69
+ value
70
+ else
71
+ mask value
72
+ end
73
+ end
74
+
75
+ def mask(value)
76
+ if value && value.respond_to?(:to_s) && !is_boolean?(value)
77
+ MASK_SYMBOL * value.to_s.length
78
+ else
79
+ MASK_SYMBOL
80
+ end
81
+ end
82
+
83
+ def is_boolean?(value)
84
+ value.is_a?(TrueClass) || value.is_a?(FalseClass)
85
+ end
86
+ end
87
+ end
88
+ end
@@ -0,0 +1,9 @@
1
+ class Logasm
2
+ module Preprocessors
3
+ def self.get(type, arguments)
4
+ require_relative "preprocessors/#{type.to_s}"
5
+ preprocessor = const_get(Inflecto.camelize(type.to_s))
6
+ preprocessor.new(arguments)
7
+ end
8
+ end
9
+ end
data/lib/logasm.rb CHANGED
@@ -4,20 +4,25 @@ require 'json'
4
4
  require_relative 'logasm/adapters'
5
5
  require_relative 'logasm/utils'
6
6
  require_relative 'logasm/null_logger'
7
+ require_relative 'logasm/preprocessors'
7
8
 
8
9
  LOG_LEVEL_QUERY_METHODS = [:debug?, :info?, :warn?, :error?, :fatal?]
9
10
 
10
11
  class Logasm
11
- def self.build(service_name, loggers_config)
12
+ def self.build(service_name, loggers_config, preprocessors_config = {})
12
13
  loggers_config ||= {stdout: nil}
14
+ preprocessors = preprocessors_config.map do |type, arguments|
15
+ Preprocessors.get(type.to_s, arguments || {})
16
+ end
13
17
  adapters = loggers_config.map do |type, arguments|
14
18
  Adapters.get(type.to_s, service_name, arguments || {})
15
19
  end
16
- new(adapters)
20
+ new(adapters, preprocessors)
17
21
  end
18
22
 
19
- def initialize(adapters)
23
+ def initialize(adapters, preprocessors)
20
24
  @adapters = adapters
25
+ @preprocessors = preprocessors
21
26
  end
22
27
 
23
28
  def debug(*args)
@@ -50,9 +55,16 @@ class Logasm
50
55
 
51
56
  def log(level, *args)
52
57
  data = parse_log_data(*args)
58
+ processed_data = preprocess(data)
53
59
 
54
60
  @adapters.each do |adapter|
55
- adapter.log(level, data)
61
+ adapter.log(level, processed_data)
62
+ end
63
+ end
64
+
65
+ def preprocess(data)
66
+ @preprocessors.inject(data) do |data_to_process, preprocessor|
67
+ preprocessor.process(data_to_process)
56
68
  end
57
69
  end
58
70
 
data/logasm.gemspec CHANGED
@@ -4,7 +4,7 @@ $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
4
4
 
5
5
  Gem::Specification.new do |gem|
6
6
  gem.name = "logasm"
7
- gem.version = '0.2.6'
7
+ gem.version = '0.3.0'
8
8
  gem.authors = ["Salemove"]
9
9
  gem.email = ["support@salemove.com"]
10
10
  gem.description = %q{It's logasmic}
data/spec/logasm_spec.rb CHANGED
@@ -38,11 +38,36 @@ describe Logasm do
38
38
 
39
39
  described_class.build('test_service', nil)
40
40
  end
41
+
42
+ it 'creates preprocessor when preprocessor defined' do
43
+ expect(described_class).to receive(:new) do |adapters, preprocessors|
44
+ expect(preprocessors.count).to be(1)
45
+ expect(preprocessors.first).to be_a(described_class::Preprocessors::Blacklist)
46
+ end
47
+
48
+ preprocessors = {blacklist: {fields: []}}
49
+ described_class.build('test_service', nil, preprocessors)
50
+ end
51
+ end
52
+
53
+ context 'when preprocessor defined' do
54
+ let(:logasm) { described_class.new([adapter], [preprocessor]) }
55
+ let(:adapter) { double }
56
+ let(:preprocessor) { double }
57
+ let(:data) { {data: 'data'} }
58
+
59
+ it 'preprocesses data before logging' do
60
+ expect(preprocessor).to receive(:process).with(data).and_return(data.merge(processed: true)).ordered
61
+ expect(adapter).to receive(:log).with(:info, data.merge(processed: true)).ordered
62
+
63
+ logasm.info(data)
64
+ end
41
65
  end
42
66
 
43
67
  context 'when parsing log data' do
44
- let(:logasm) { described_class.new([adapter]) }
68
+ let(:logasm) { described_class.new([adapter], preprocessors) }
45
69
  let(:adapter) { double }
70
+ let(:preprocessors) { [] }
46
71
 
47
72
  it 'parses empty string with nil metadata' do
48
73
  expect(adapter).to receive(:log).with(:info, message: '')
@@ -114,4 +139,10 @@ describe Logasm do
114
139
  end
115
140
  end
116
141
  end
142
+
143
+ it 'has the same interface as Ruby logger' do
144
+ skip "https://salemove.atlassian.net/browse/INF-464"
145
+ logger = described_class.build('test_service', stdout: {level: 'debug'})
146
+ expect(logger).to implement_interface(Logger)
147
+ end
117
148
  end
@@ -0,0 +1,131 @@
1
+ require 'spec_helper'
2
+ require_relative '../../lib/logasm/preprocessors/blacklist'
3
+
4
+ describe Logasm::Preprocessors::Blacklist do
5
+ subject(:processed_data) { described_class.new(config).process(data) }
6
+
7
+ let(:config) {{
8
+ fields: [{ key: 'field', action: action }]
9
+ }}
10
+
11
+ let(:action) {}
12
+ let(:data) {{
13
+ field: value,
14
+ data: {
15
+ field: 'secret'
16
+ },
17
+ array: [{field: 'secret'}]
18
+ }}
19
+
20
+ let(:value) { 'secret' }
21
+
22
+ context 'when action is unsupported' do
23
+ let(:action) { 'reverse' }
24
+
25
+ it 'throws exception' do
26
+ expect { processed_data }.to raise_exception(Logasm::Preprocessors::Blacklist::UnsupportedActionException)
27
+ end
28
+ end
29
+
30
+ context 'when action is "exclude"' do
31
+ let(:action) { 'exclude' }
32
+
33
+ it 'removes the field' do
34
+ expect(processed_data).not_to include(:field)
35
+ end
36
+
37
+ it 'removes nested field' do
38
+ expect(processed_data).not_to include_at_depth({field: 'secret'}, 1)
39
+ end
40
+
41
+ it 'removes nested in array field' do
42
+ expect(processed_data[:array]).not_to include({field: 'secret'})
43
+ end
44
+
45
+ context 'when field is deeply nested' do
46
+ let(:depth) { 10 }
47
+ let(:data) { data_with_nested_field({field: 'secret'}, depth) }
48
+
49
+ it 'removes deeply nested field' do
50
+ expect(processed_data).not_to include_at_depth({field: 'secret'}, depth)
51
+ end
52
+ end
53
+ end
54
+
55
+ context 'when action is "mask"' do
56
+ let(:action) { 'mask' }
57
+
58
+ it 'masks nested field' do
59
+ expect(processed_data).to include_at_depth({field: '******'}, 1)
60
+ end
61
+
62
+ it 'masks nested in array field' do
63
+ expect(processed_data[:array]).to include({field: '******'})
64
+ end
65
+
66
+ context 'when field is string' do
67
+ let(:value) { 'secret' }
68
+
69
+ it 'masks value with asterisks' do
70
+ expect(processed_data).to include(field: '******')
71
+ end
72
+ end
73
+
74
+ context 'when field is number' do
75
+ let(:value) { 42 }
76
+
77
+ it 'masks number value' do
78
+ expect(processed_data).to include(field: '**')
79
+ end
80
+ end
81
+
82
+ context 'when field is boolean' do
83
+ let(:value) { true }
84
+
85
+ it 'masks value with one asterisk' do
86
+ expect(processed_data).to include(field: '*')
87
+ end
88
+ end
89
+
90
+ context 'when field is array' do
91
+ let(:value) { [1,2,3,4] }
92
+
93
+ it 'masks value with one asterisk' do
94
+ expect(processed_data).to include(field: '*')
95
+ end
96
+ end
97
+
98
+ context 'when field is hash' do
99
+ let(:value) { {data: {}} }
100
+
101
+ it 'masks value with one asterisk' do
102
+ expect(processed_data).to include(field: '*')
103
+ end
104
+ end
105
+
106
+ context 'when field is deeply nested' do
107
+ let(:depth) { 10 }
108
+ let(:data) { data_with_nested_field({field: 'secret'}, depth) }
109
+
110
+ it 'masks deeply nested field' do
111
+ expect(processed_data).to include_at_depth({field: '******'}, depth)
112
+ end
113
+ end
114
+ end
115
+
116
+ def data_with_nested_field(field, depth)
117
+ depth.times.inject(field) do |mem|
118
+ {}.merge(data: mem)
119
+ end
120
+ end
121
+
122
+ RSpec::Matchers.define :include_at_depth do |expected_hash, depth|
123
+ match do |actual|
124
+ nested_data = depth.times.inject(actual) do |mem|
125
+ mem[:data]
126
+ end
127
+
128
+ expect(nested_data).to include(expected_hash)
129
+ end
130
+ end
131
+ end
@@ -0,0 +1,152 @@
1
+ require 'spec_helper'
2
+ require_relative '../../lib/logasm/preprocessors/whitelist'
3
+
4
+ describe Logasm::Preprocessors::Whitelist do
5
+ subject(:processed_data) { described_class.new(config).process(data) }
6
+
7
+ let(:config) { {pointers: pointers} }
8
+ let(:pointers) { [] }
9
+ let(:data) {{
10
+ field: 'secret',
11
+ data: {
12
+ field: 'secret'
13
+ },
14
+ array: [{field: 'secret'}]
15
+ }}
16
+
17
+ it 'masks all non-whitelisted fields' do
18
+ expect(processed_data).to eq({
19
+ field: '******',
20
+ data: {
21
+ field: '******'
22
+ },
23
+ array: [{field: '******'}]
24
+ })
25
+ end
26
+
27
+ context 'when pointer has trailing slash' do
28
+ let(:pointers) { ['/field/'] }
29
+
30
+ it 'throws exception' do
31
+ expect { processed_data }.to raise_exception(Logasm::Preprocessors::Whitelist::InvalidPointerFormatException)
32
+ end
33
+ end
34
+
35
+ context 'with whitelisted field' do
36
+ let(:pointers) { ['/field'] }
37
+
38
+ it 'includes the field' do
39
+ expect(processed_data).to eq({
40
+ field: 'secret',
41
+ data: {
42
+ field: '******'
43
+ },
44
+ array: [{field: '******'}]
45
+ })
46
+ end
47
+ end
48
+
49
+ context 'with whitelisted nested field' do
50
+ let(:pointers) { ['/data/field'] }
51
+
52
+ it 'includes nested field' do
53
+ expect(processed_data).to eq({
54
+ field: '******',
55
+ data: {
56
+ field: 'secret'
57
+ },
58
+ array: [{field: '******'}]
59
+ })
60
+ end
61
+ end
62
+
63
+ context 'with whitelisted array element field' do
64
+ let(:pointers) { ['/array/0/field'] }
65
+
66
+ it 'includes array element' do
67
+ expect(processed_data).to eq({
68
+ field: '******',
69
+ data: {
70
+ field: '******'
71
+ },
72
+ array: [{field: 'secret'}]
73
+ })
74
+ end
75
+ end
76
+
77
+ context 'with whitelisted array element' do
78
+ let(:pointers) { ['/array/0'] }
79
+
80
+ it 'masks array element' do
81
+ expect(processed_data).to include(array: [{field: '******'}])
82
+ end
83
+ end
84
+
85
+ context 'with whitelisted array' do
86
+ let(:pointers) { ['/array'] }
87
+
88
+ it 'masks array' do
89
+ expect(processed_data).to include(array: [{field: '******'}])
90
+ end
91
+ end
92
+
93
+ context 'with whitelisted hash' do
94
+ let(:pointers) { ['/data'] }
95
+
96
+ it 'masks hash' do
97
+ expect(processed_data).to include(data: {field: '******'})
98
+ end
99
+ end
100
+
101
+ context 'when boolean present' do
102
+ let(:data) { {bool: true} }
103
+
104
+ it 'masks it with single asteriks' do
105
+ expect(processed_data).to eq(bool: '*')
106
+ end
107
+ end
108
+
109
+ context 'when field has slash in the name' do
110
+ let(:data) {{
111
+ 'field_with_/' => 'secret'
112
+ }}
113
+ let(:pointers) { ['/field_with_~1'] }
114
+
115
+ it 'includes field' do
116
+ expect(processed_data).to include('field_with_/'=> 'secret')
117
+ end
118
+ end
119
+
120
+ context 'when field has tilde in the name' do
121
+ let(:data) {{
122
+ 'field_with_~' => 'secret'
123
+ }}
124
+ let(:pointers) { ['/field_with_~0'] }
125
+
126
+ it 'includes field' do
127
+ expect(processed_data).to include('field_with_~'=> 'secret')
128
+ end
129
+ end
130
+
131
+ context 'when field has slash in the name' do
132
+ let(:data) {{
133
+ 'field_with_/' => 'secret'
134
+ }}
135
+ let(:pointers) { ['/field_with_~1'] }
136
+
137
+ it 'includes field' do
138
+ expect(processed_data).to include('field_with_/'=> 'secret')
139
+ end
140
+ end
141
+
142
+ context 'when field has tilde in the name' do
143
+ let(:data) {{
144
+ 'field_with_~' => 'secret'
145
+ }}
146
+ let(:pointers) { ['/field_with_~0'] }
147
+
148
+ it 'includes field' do
149
+ expect(processed_data).to include('field_with_~'=> 'secret')
150
+ end
151
+ end
152
+ end
@@ -0,0 +1,11 @@
1
+ RSpec::Matchers.define :implement_interface do |expected|
2
+ required_methods = expected.instance_methods(false)
3
+ match do |actual|
4
+ required_methods - actual.methods == []
5
+ end
6
+
7
+ failure_message do |actual|
8
+ missing_methods = required_methods - actual.methods
9
+ "Expected instance methods #{missing_methods.inspect} to be implemented"
10
+ end
11
+ end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: logasm
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.2.6
4
+ version: 0.3.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Salemove
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2016-06-30 00:00:00.000000000 Z
11
+ date: 2016-07-18 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: inflecto
@@ -88,6 +88,9 @@ files:
88
88
  - lib/logasm/adapters/rabbitmq_adapter.rb
89
89
  - lib/logasm/adapters/stdout_adapter.rb
90
90
  - lib/logasm/null_logger.rb
91
+ - lib/logasm/preprocessors.rb
92
+ - lib/logasm/preprocessors/blacklist.rb
93
+ - lib/logasm/preprocessors/whitelist.rb
91
94
  - lib/logasm/utils.rb
92
95
  - logasm.gemspec
93
96
  - spec/adapters/logstash_adapter/formatter_spec.rb
@@ -95,8 +98,11 @@ files:
95
98
  - spec/adapters/rabbitmq_adapter_spec.rb
96
99
  - spec/adapters/stdout_adapter_spec.rb
97
100
  - spec/logasm_spec.rb
101
+ - spec/preprocessors/blacklist_spec.rb
102
+ - spec/preprocessors/whitelist_spec.rb
98
103
  - spec/spec_helper.rb
99
104
  - spec/support/freddy_mock.rb
105
+ - spec/support/implement_interface.rb
100
106
  - spec/utils_spec.rb
101
107
  homepage:
102
108
  licenses:
@@ -118,7 +124,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
118
124
  version: '0'
119
125
  requirements: []
120
126
  rubyforge_project:
121
- rubygems_version: 2.4.5.1
127
+ rubygems_version: 2.4.8
122
128
  signing_key:
123
129
  specification_version: 4
124
130
  summary: What description said
@@ -128,6 +134,9 @@ test_files:
128
134
  - spec/adapters/rabbitmq_adapter_spec.rb
129
135
  - spec/adapters/stdout_adapter_spec.rb
130
136
  - spec/logasm_spec.rb
137
+ - spec/preprocessors/blacklist_spec.rb
138
+ - spec/preprocessors/whitelist_spec.rb
131
139
  - spec/spec_helper.rb
132
140
  - spec/support/freddy_mock.rb
141
+ - spec/support/implement_interface.rb
133
142
  - spec/utils_spec.rb