lenjador 1.2.1 → 1.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 +4 -4
- data/.rubocop.yml +38 -0
- data/.travis.yml +0 -1
- data/Gemfile +1 -6
- data/Rakefile +6 -4
- data/benchmark/whitelisting.rb +7 -7
- data/lenjador.gemspec +19 -20
- data/lib/lenjador.rb +5 -3
- data/lib/lenjador/adapters.rb +11 -11
- data/lib/lenjador/adapters/stdout_adapter.rb +3 -1
- data/lib/lenjador/adapters/stdout_json_adapter.rb +10 -8
- data/lib/lenjador/null_logger.rb +7 -10
- data/lib/lenjador/preprocessors.rb +2 -0
- data/lib/lenjador/preprocessors/blacklist.rb +8 -7
- data/lib/lenjador/preprocessors/json_pointer_trie.rb +4 -2
- data/lib/lenjador/preprocessors/strategies/mask.rb +3 -1
- data/lib/lenjador/preprocessors/strategies/prune.rb +2 -0
- data/lib/lenjador/preprocessors/whitelist.rb +5 -5
- data/lib/lenjador/utils.rb +34 -7
- data/profiler/logs.rb +19 -0
- data/spec/{adapters → lenjador/adapters}/stdout_adapter_spec.rb +1 -1
- data/spec/{adapters → lenjador/adapters}/stdout_json_adapter_spec.rb +1 -1
- data/spec/{preprocessors → lenjador/preprocessors}/blacklist_spec.rb +18 -14
- data/spec/{preprocessors/json_pointer_trie.rb → lenjador/preprocessors/json_pointer_trie_spec.rb} +4 -4
- data/spec/lenjador/preprocessors/whitelist_spec.rb +335 -0
- data/spec/lenjador/utils_spec.rb +128 -0
- data/spec/lenjador_spec.rb +2 -8
- data/spec/spec_helper.rb +1 -1
- metadata +60 -16
- data/spec/preprocessors/whitelist_spec.rb +0 -335
- data/spec/utils_spec.rb +0 -84
data/lib/lenjador/utils.rb
CHANGED
@@ -1,8 +1,11 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
1
3
|
require 'time'
|
2
4
|
|
3
5
|
class Lenjador
|
4
6
|
module Utils
|
5
7
|
DECIMAL_FRACTION_OF_SECOND = 3
|
8
|
+
NO_TRACE_INFORMATION = {}.freeze
|
6
9
|
|
7
10
|
# Build logstash json compatible event
|
8
11
|
#
|
@@ -13,8 +16,9 @@ class Lenjador
|
|
13
16
|
# @return [Hash]
|
14
17
|
def self.build_event(metadata, level, application_name)
|
15
18
|
overwritable_params
|
16
|
-
.merge(metadata)
|
17
|
-
.merge(
|
19
|
+
.merge!(metadata)
|
20
|
+
.merge!(tracing_information)
|
21
|
+
.merge!(
|
18
22
|
application: application_name,
|
19
23
|
level: level
|
20
24
|
)
|
@@ -37,6 +41,7 @@ class Lenjador
|
|
37
41
|
:@timestamp => Time.now.utc.iso8601(DECIMAL_FRACTION_OF_SECOND)
|
38
42
|
}
|
39
43
|
end
|
44
|
+
private_class_method :overwritable_params
|
40
45
|
|
41
46
|
def self.serialize_time_objects!(object)
|
42
47
|
if object.is_a?(Hash)
|
@@ -67,7 +72,7 @@ class Lenjador
|
|
67
72
|
end
|
68
73
|
else
|
69
74
|
require 'oj'
|
70
|
-
DUMP_OPTIONS = {
|
75
|
+
DUMP_OPTIONS = {mode: :compat, time_format: :ruby}.freeze
|
71
76
|
|
72
77
|
def self.generate_json(obj)
|
73
78
|
serialize_time_objects!(obj)
|
@@ -79,13 +84,35 @@ class Lenjador
|
|
79
84
|
def self.underscore(input)
|
80
85
|
word = input.to_s.dup
|
81
86
|
word.gsub!(/::/, '/')
|
82
|
-
word.gsub!(/([A-Z]+)([A-Z][a-z])/,'\1_\2')
|
83
|
-
word.gsub!(/([a-z\d])([A-Z])/,'\1_\2')
|
84
|
-
word.tr!(
|
87
|
+
word.gsub!(/([A-Z]+)([A-Z][a-z])/, '\1_\2')
|
88
|
+
word.gsub!(/([a-z\d])([A-Z])/, '\1_\2')
|
89
|
+
word.tr!('-', '_')
|
85
90
|
word.downcase!
|
86
91
|
word
|
87
92
|
end
|
88
93
|
|
89
|
-
|
94
|
+
# Tracing information
|
95
|
+
#
|
96
|
+
# Tracing information is included only if OpenTracing is defined and if it
|
97
|
+
# supports method called `active_span` (version >= 0.4.1). We use
|
98
|
+
# SpanContext#trace_id and SpanContext#span_id methods to retrieve tracing
|
99
|
+
# information. These methods are not yet supported by the OpenTracing API,
|
100
|
+
# so we first check if these methods exist. Popular tracing libraries
|
101
|
+
# already implement them. These methods are likely to be added to the API
|
102
|
+
# very soon: https://github.com/opentracing/specification/blob/master/rfc/trace_identifiers.md
|
103
|
+
def self.tracing_information
|
104
|
+
return NO_TRACE_INFORMATION if !defined?(OpenTracing) || !OpenTracing.respond_to?(:active_span)
|
105
|
+
|
106
|
+
context = OpenTracing.active_span&.context
|
107
|
+
if context && context.respond_to?(:trace_id) && context.respond_to?(:span_id)
|
108
|
+
{
|
109
|
+
trace_id: context.trace_id,
|
110
|
+
span_id: context.span_id
|
111
|
+
}
|
112
|
+
else
|
113
|
+
NO_TRACE_INFORMATION
|
114
|
+
end
|
115
|
+
end
|
116
|
+
private_class_method :tracing_information
|
90
117
|
end
|
91
118
|
end
|
data/profiler/logs.rb
ADDED
@@ -0,0 +1,19 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
# Run with `ruby profiler/logs.rb > /dev/null` and then you can read the
|
4
|
+
# results using `open /tmp/lenjador.html`
|
5
|
+
|
6
|
+
require 'bundler/setup'
|
7
|
+
Bundler.require
|
8
|
+
logger = Lenjador.build('test_service', stdout: {level: 'info', json: true})
|
9
|
+
|
10
|
+
require 'ruby-prof'
|
11
|
+
RubyProf.start
|
12
|
+
|
13
|
+
100_000.times do
|
14
|
+
logger.info 'hello there', a: 'asdf', b: 'eadsfasdf', c: {hello: 'there'}
|
15
|
+
end
|
16
|
+
|
17
|
+
result = RubyProf.stop
|
18
|
+
printer = RubyProf::GraphHtmlPrinter.new(result)
|
19
|
+
File.open('/tmp/lenjador.html', 'w+') { |file| printer.print(file) }
|
@@ -1,21 +1,25 @@
|
|
1
1
|
require 'spec_helper'
|
2
|
-
require_relative '
|
2
|
+
require_relative '../../../lib/lenjador/preprocessors/blacklist'
|
3
3
|
|
4
4
|
describe Lenjador::Preprocessors::Blacklist do
|
5
5
|
subject(:processed_data) { described_class.new(config).process(data) }
|
6
6
|
|
7
|
-
let(:config)
|
8
|
-
|
9
|
-
|
7
|
+
let(:config) do
|
8
|
+
{
|
9
|
+
fields: [{key: 'field', action: action}]
|
10
|
+
}
|
11
|
+
end
|
10
12
|
|
11
13
|
let(:action) {}
|
12
|
-
let(:data)
|
13
|
-
|
14
|
-
|
15
|
-
|
16
|
-
|
17
|
-
|
18
|
-
|
14
|
+
let(:data) do
|
15
|
+
{
|
16
|
+
field: value,
|
17
|
+
data: {
|
18
|
+
field: 'secret'
|
19
|
+
},
|
20
|
+
array: [{field: 'secret'}]
|
21
|
+
}
|
22
|
+
end
|
19
23
|
|
20
24
|
let(:value) { 'secret' }
|
21
25
|
|
@@ -39,7 +43,7 @@ describe Lenjador::Preprocessors::Blacklist do
|
|
39
43
|
end
|
40
44
|
|
41
45
|
it 'removes nested in array field' do
|
42
|
-
expect(processed_data[:array]).not_to include(
|
46
|
+
expect(processed_data[:array]).not_to include(field: 'secret')
|
43
47
|
end
|
44
48
|
|
45
49
|
context 'when field is deeply nested' do
|
@@ -60,7 +64,7 @@ describe Lenjador::Preprocessors::Blacklist do
|
|
60
64
|
end
|
61
65
|
|
62
66
|
it 'masks nested in array field' do
|
63
|
-
expect(processed_data[:array]).to include(
|
67
|
+
expect(processed_data[:array]).to include(field: '*****')
|
64
68
|
end
|
65
69
|
|
66
70
|
context 'when field is string' do
|
@@ -88,7 +92,7 @@ describe Lenjador::Preprocessors::Blacklist do
|
|
88
92
|
end
|
89
93
|
|
90
94
|
context 'when field is array' do
|
91
|
-
let(:value) { [1,2,3,4] }
|
95
|
+
let(:value) { [1, 2, 3, 4] }
|
92
96
|
|
93
97
|
it 'masks value with asterisks' do
|
94
98
|
expect(processed_data).to include(field: '*****')
|
data/spec/{preprocessors/json_pointer_trie.rb → lenjador/preprocessors/json_pointer_trie_spec.rb}
RENAMED
@@ -20,9 +20,9 @@ RSpec.describe Lenjador::Preprocessors::JSONPointerTrie do
|
|
20
20
|
it 'returns false if trie does not contain requested prefix or value' do
|
21
21
|
trie.insert('/data/nested/key')
|
22
22
|
|
23
|
-
expect(trie).
|
24
|
-
expect(trie).
|
25
|
-
expect(trie).
|
23
|
+
expect(trie).not_to include('/bad_data')
|
24
|
+
expect(trie).not_to include('/data/bad_nested')
|
25
|
+
expect(trie).not_to include('/data/nested/bad_key')
|
26
26
|
end
|
27
27
|
|
28
28
|
it 'returns true if trie contains requested prefix under wildcard' do
|
@@ -30,7 +30,7 @@ RSpec.describe Lenjador::Preprocessors::JSONPointerTrie do
|
|
30
30
|
|
31
31
|
expect(trie).to include('/data/arbitrary_key/key')
|
32
32
|
expect(trie).to include('/data/another_key/key')
|
33
|
-
expect(trie).
|
33
|
+
expect(trie).not_to include('/data/arbitrary_key/bad_nested_key')
|
34
34
|
end
|
35
35
|
end
|
36
36
|
end
|
@@ -0,0 +1,335 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
require_relative '../../../lib/lenjador/preprocessors/whitelist'
|
3
|
+
|
4
|
+
RSpec.describe Lenjador::Preprocessors::Whitelist do
|
5
|
+
context 'when :action is :mask or omitted' do
|
6
|
+
subject(:processed_data) { described_class.new(config).process(data) }
|
7
|
+
|
8
|
+
let(:config) { {pointers: pointers} }
|
9
|
+
let(:pointers) { [] }
|
10
|
+
let(:data) do
|
11
|
+
{
|
12
|
+
field: 'secret',
|
13
|
+
data: {
|
14
|
+
field: 'secret'
|
15
|
+
},
|
16
|
+
array: [{field: 'secret'}]
|
17
|
+
}
|
18
|
+
end
|
19
|
+
|
20
|
+
it 'masks all non-whitelisted fields' do
|
21
|
+
expect(processed_data).to eq(
|
22
|
+
field: '*****',
|
23
|
+
data: '*****',
|
24
|
+
array: '*****'
|
25
|
+
)
|
26
|
+
end
|
27
|
+
|
28
|
+
context 'when pointer has trailing slash' do
|
29
|
+
let(:pointers) { ['/field/'] }
|
30
|
+
|
31
|
+
it 'throws exception' do
|
32
|
+
expect { processed_data }.to raise_exception(Lenjador::Preprocessors::Whitelist::InvalidPointerFormatException)
|
33
|
+
end
|
34
|
+
end
|
35
|
+
|
36
|
+
context 'with whitelisted field' do
|
37
|
+
let(:pointers) { ['/field'] }
|
38
|
+
|
39
|
+
it 'includes the field' do
|
40
|
+
expect(processed_data).to eq(
|
41
|
+
field: 'secret',
|
42
|
+
data: '*****',
|
43
|
+
array: '*****'
|
44
|
+
)
|
45
|
+
end
|
46
|
+
end
|
47
|
+
|
48
|
+
context 'with whitelisted nested field' do
|
49
|
+
let(:pointers) { ['/data/field'] }
|
50
|
+
|
51
|
+
it 'includes nested field' do
|
52
|
+
expect(processed_data).to eq(
|
53
|
+
field: '*****',
|
54
|
+
data: {
|
55
|
+
field: 'secret'
|
56
|
+
},
|
57
|
+
array: '*****'
|
58
|
+
)
|
59
|
+
end
|
60
|
+
end
|
61
|
+
|
62
|
+
context 'with whitelisted array element field' do
|
63
|
+
let(:pointers) { ['/array/0/field'] }
|
64
|
+
|
65
|
+
it 'includes array element' do
|
66
|
+
expect(processed_data).to eq(
|
67
|
+
field: '*****',
|
68
|
+
data: '*****',
|
69
|
+
array: [{field: 'secret'}]
|
70
|
+
)
|
71
|
+
end
|
72
|
+
end
|
73
|
+
|
74
|
+
context 'with whitelisted hash' do
|
75
|
+
it 'includes all whitelisted hash elements' do
|
76
|
+
source = {foo: {bar: 'baz'}}
|
77
|
+
target = {foo: {bar: 'baz'}}
|
78
|
+
expect(process(['/foo/~'], source)).to eq(target)
|
79
|
+
end
|
80
|
+
|
81
|
+
it 'does not include nested elements' do
|
82
|
+
source = {foo: {bar: {baz: 'asd'}}}
|
83
|
+
target = {foo: {bar: {baz: '*****'}}}
|
84
|
+
expect(process(['/foo/~'], source)).to eq(target)
|
85
|
+
end
|
86
|
+
end
|
87
|
+
|
88
|
+
context 'with whitelisted array elements field with wildcard' do
|
89
|
+
let(:data) do
|
90
|
+
{
|
91
|
+
array: [
|
92
|
+
{field: 'data1', secret: 'secret1'},
|
93
|
+
{field: 'data2', secret: 'secret2'}
|
94
|
+
]
|
95
|
+
}
|
96
|
+
end
|
97
|
+
let(:pointers) { ['/array/~/field'] }
|
98
|
+
|
99
|
+
it 'includes array elements field' do
|
100
|
+
expect(processed_data).to include(
|
101
|
+
array: [
|
102
|
+
{field: 'data1', secret: '*****'},
|
103
|
+
{field: 'data2', secret: '*****'}
|
104
|
+
]
|
105
|
+
)
|
106
|
+
end
|
107
|
+
end
|
108
|
+
|
109
|
+
context 'with whitelisted string array elements with wildcard' do
|
110
|
+
let(:data) do
|
111
|
+
{array: %w[secret secret]}
|
112
|
+
end
|
113
|
+
let(:pointers) { ['/array/~'] }
|
114
|
+
|
115
|
+
it 'includes array elements' do
|
116
|
+
expect(processed_data).to include(array: %w[secret secret])
|
117
|
+
end
|
118
|
+
end
|
119
|
+
|
120
|
+
context 'with whitelisted string array elements in an array with wildcard' do
|
121
|
+
let(:data) do
|
122
|
+
{
|
123
|
+
nested: [{array: %w[secret secret]}]
|
124
|
+
}
|
125
|
+
end
|
126
|
+
let(:pointers) { ['/nested/~/array/~'] }
|
127
|
+
|
128
|
+
it 'includes array elements' do
|
129
|
+
expect(processed_data).to include(nested: [{array: %w[secret secret]}])
|
130
|
+
end
|
131
|
+
end
|
132
|
+
|
133
|
+
context 'with whitelisted array element' do
|
134
|
+
let(:pointers) { ['/array/0'] }
|
135
|
+
|
136
|
+
it 'masks array element' do
|
137
|
+
expect(processed_data).to include(array: [{field: '*****'}])
|
138
|
+
end
|
139
|
+
end
|
140
|
+
|
141
|
+
context 'with whitelisted array' do
|
142
|
+
let(:pointers) { ['/array'] }
|
143
|
+
|
144
|
+
it 'masks array' do
|
145
|
+
expect(processed_data).to include(array: ['*****'])
|
146
|
+
end
|
147
|
+
end
|
148
|
+
|
149
|
+
context 'with whitelisted hash' do
|
150
|
+
let(:pointers) { ['/data'] }
|
151
|
+
|
152
|
+
it 'masks hash' do
|
153
|
+
expect(processed_data).to include(data: {field: '*****'})
|
154
|
+
end
|
155
|
+
end
|
156
|
+
|
157
|
+
context 'when boolean present' do
|
158
|
+
let(:data) { {bool: true} }
|
159
|
+
|
160
|
+
it 'masks it with asteriks' do
|
161
|
+
expect(processed_data).to eq(bool: '*****')
|
162
|
+
end
|
163
|
+
end
|
164
|
+
|
165
|
+
context 'when field has slash in the name' do
|
166
|
+
let(:data) do
|
167
|
+
{'field_with_/' => 'secret'}
|
168
|
+
end
|
169
|
+
let(:pointers) { ['/field_with_~1'] }
|
170
|
+
|
171
|
+
it 'includes field' do
|
172
|
+
expect(processed_data).to include('field_with_/' => 'secret')
|
173
|
+
end
|
174
|
+
end
|
175
|
+
|
176
|
+
context 'when field has tilde in the name' do
|
177
|
+
let(:data) do
|
178
|
+
{'field_with_~' => 'secret'}
|
179
|
+
end
|
180
|
+
let(:pointers) { ['/field_with_~0'] }
|
181
|
+
|
182
|
+
it 'includes field' do
|
183
|
+
expect(processed_data).to include('field_with_~' => 'secret')
|
184
|
+
end
|
185
|
+
end
|
186
|
+
|
187
|
+
context 'when field has tilde and 1' do
|
188
|
+
let(:data) do
|
189
|
+
{'field_with_~1' => 'secret'}
|
190
|
+
end
|
191
|
+
let(:pointers) { ['/field_with_~01'] }
|
192
|
+
|
193
|
+
it 'includes field' do
|
194
|
+
expect(processed_data).to include('field_with_~1' => 'secret')
|
195
|
+
end
|
196
|
+
end
|
197
|
+
|
198
|
+
def process(pointers, data)
|
199
|
+
described_class.new(pointers: pointers).process(data)
|
200
|
+
end
|
201
|
+
end
|
202
|
+
|
203
|
+
context 'when :action is :exclude or :prune' do
|
204
|
+
subject(:processed_data) { described_class.new(config).process(data) }
|
205
|
+
|
206
|
+
let(:config) { {pointers: pointers, action: :prune} }
|
207
|
+
let(:pointers) { [] }
|
208
|
+
let(:data) do
|
209
|
+
{
|
210
|
+
field: 'secret',
|
211
|
+
data: {
|
212
|
+
field: 'secret'
|
213
|
+
},
|
214
|
+
array: [{field: 'secret'}, {field2: 'secret'}]
|
215
|
+
}
|
216
|
+
end
|
217
|
+
|
218
|
+
context 'when pointers is empty' do
|
219
|
+
it 'prunes all fields from the input' do
|
220
|
+
expect(processed_data).to eq({})
|
221
|
+
end
|
222
|
+
end
|
223
|
+
|
224
|
+
context 'with whitelisted field' do
|
225
|
+
let(:pointers) { ['/field'] }
|
226
|
+
|
227
|
+
it 'includes the field' do
|
228
|
+
expect(processed_data).to eq(field: 'secret')
|
229
|
+
end
|
230
|
+
end
|
231
|
+
|
232
|
+
context 'with whitelisted nested field' do
|
233
|
+
let(:pointers) { ['/data/field'] }
|
234
|
+
|
235
|
+
it 'includes nested field' do
|
236
|
+
expect(processed_data).to eq(data: {field: 'secret'})
|
237
|
+
end
|
238
|
+
end
|
239
|
+
|
240
|
+
context 'with whitelisted array element field' do
|
241
|
+
let(:pointers) { ['/array/0/field'] }
|
242
|
+
|
243
|
+
it 'includes array element' do
|
244
|
+
expect(processed_data).to eq(array: [{field: 'secret'}])
|
245
|
+
end
|
246
|
+
end
|
247
|
+
|
248
|
+
context 'with whitelisted hash' do
|
249
|
+
it 'includes all whitelisted hash elements' do
|
250
|
+
source = {foo: {bar: 'baz'}}
|
251
|
+
target = {foo: {bar: 'baz'}}
|
252
|
+
expect(process(['/foo/~'], source)).to eq(target)
|
253
|
+
end
|
254
|
+
|
255
|
+
it 'does not include nested elements' do
|
256
|
+
source = {foo: {bar: {baz: 'asd'}}}
|
257
|
+
target = {foo: {bar: {}}}
|
258
|
+
expect(process(['/foo/~'], source)).to eq(target)
|
259
|
+
end
|
260
|
+
end
|
261
|
+
|
262
|
+
context 'with whitelisted array elements field with wildcard' do
|
263
|
+
let(:data) do
|
264
|
+
{
|
265
|
+
array: [
|
266
|
+
{field: 'data1', secret: 'secret1'},
|
267
|
+
{field: 'data2', secret: 'secret2'}
|
268
|
+
]
|
269
|
+
}
|
270
|
+
end
|
271
|
+
let(:pointers) { ['/array/~/field'] }
|
272
|
+
|
273
|
+
it 'includes array elements field' do
|
274
|
+
expect(processed_data).to include(
|
275
|
+
array: [
|
276
|
+
{field: 'data1'},
|
277
|
+
{field: 'data2'}
|
278
|
+
]
|
279
|
+
)
|
280
|
+
end
|
281
|
+
end
|
282
|
+
|
283
|
+
context 'with whitelisted string array elements with wildcard' do
|
284
|
+
let(:data) do
|
285
|
+
{array: %w[secret1 secret2]}
|
286
|
+
end
|
287
|
+
let(:pointers) { ['/array/~'] }
|
288
|
+
|
289
|
+
it 'includes array elements' do
|
290
|
+
expect(processed_data).to include(array: %w[secret1 secret2])
|
291
|
+
end
|
292
|
+
end
|
293
|
+
|
294
|
+
context 'with whitelisted string array elements in an array with wildcard' do
|
295
|
+
let(:data) do
|
296
|
+
{
|
297
|
+
nested: [{array: %w[secret1 secret2]}]
|
298
|
+
}
|
299
|
+
end
|
300
|
+
let(:pointers) { ['/nested/~/array/~'] }
|
301
|
+
|
302
|
+
it 'includes array elements' do
|
303
|
+
expect(processed_data).to include(nested: [{array: %w[secret1 secret2]}])
|
304
|
+
end
|
305
|
+
end
|
306
|
+
|
307
|
+
context 'with whitelisted array element' do
|
308
|
+
let(:pointers) { ['/array/0'] }
|
309
|
+
|
310
|
+
it 'masks array element' do
|
311
|
+
expect(processed_data).to eq(array: [{}])
|
312
|
+
end
|
313
|
+
end
|
314
|
+
|
315
|
+
context 'with whitelisted array' do
|
316
|
+
let(:pointers) { ['/array'] }
|
317
|
+
|
318
|
+
it 'masks array' do
|
319
|
+
expect(processed_data).to include(array: [])
|
320
|
+
end
|
321
|
+
end
|
322
|
+
|
323
|
+
context 'with whitelisted hash' do
|
324
|
+
let(:pointers) { ['/data'] }
|
325
|
+
|
326
|
+
it 'masks hash' do
|
327
|
+
expect(processed_data).to include(data: {})
|
328
|
+
end
|
329
|
+
end
|
330
|
+
|
331
|
+
def process(pointers, data)
|
332
|
+
described_class.new(pointers: pointers, action: :prune).process(data)
|
333
|
+
end
|
334
|
+
end
|
335
|
+
end
|