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.
@@ -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