lenjador 1.2.1 → 1.3.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -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 = { mode: :compat, time_format: :ruby }.freeze
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
- private_class_method :overwritable_params
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
@@ -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,5 +1,5 @@
1
1
  require 'spec_helper'
2
- require_relative '../../lib/lenjador/adapters/stdout_adapter'
2
+ require 'lenjador/adapters/stdout_adapter'
3
3
 
4
4
  describe Lenjador::Adapters::StdoutAdapter do
5
5
  it 'creates a stdout logger' do
@@ -1,6 +1,6 @@
1
1
  require 'spec_helper'
2
2
  require 'json'
3
- require_relative '../../lib/lenjador/adapters/stdout_json_adapter'
3
+ require 'lenjador/adapters/stdout_json_adapter'
4
4
 
5
5
  describe Lenjador::Adapters::StdoutJsonAdapter do
6
6
  let(:debug_level_code) { 0 }
@@ -1,21 +1,25 @@
1
1
  require 'spec_helper'
2
- require_relative '../../lib/lenjador/preprocessors/blacklist'
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
- fields: [{ key: 'field', action: action }]
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
- field: value,
14
- data: {
15
- field: 'secret'
16
- },
17
- array: [{field: 'secret'}]
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({field: 'secret'})
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({field: '*****'})
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: '*****')
@@ -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).to_not include('/bad_data')
24
- expect(trie).to_not include('/data/bad_nested')
25
- expect(trie).to_not include('/data/nested/bad_key')
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).to_not include('/data/arbitrary_key/bad_nested_key')
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