lenjador 1.2.1 → 2.1.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 +5 -5
- data/.rubocop.yml +41 -0
- data/.ruby-version +1 -1
- data/.travis.yml +1 -3
- data/Gemfile +1 -6
- data/Rakefile +6 -4
- data/benchmark/whitelisting.rb +7 -7
- data/lenjador.gemspec +21 -26
- data/lib/lenjador.rb +38 -23
- data/lib/lenjador/adapters.rb +9 -14
- data/lib/lenjador/adapters/stdout_adapter.rb +6 -9
- data/lib/lenjador/adapters/stdout_json_adapter.rb +10 -36
- data/lib/lenjador/null_logger.rb +7 -10
- data/lib/lenjador/preprocessors.rb +2 -0
- data/lib/lenjador/preprocessors/blacklist.rb +11 -9
- 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 +6 -6
- data/lib/lenjador/utils.rb +42 -43
- data/profiler/logs.rb +19 -0
- data/spec/lenjador/adapters/stdout_adapter_spec.rb +48 -0
- data/spec/lenjador/adapters/stdout_json_adapter_spec.rb +46 -0
- 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 +319 -0
- data/spec/lenjador/utils_spec.rb +99 -0
- data/spec/lenjador_spec.rb +41 -33
- data/spec/spec_helper.rb +1 -1
- metadata +67 -24
- data/spec/adapters/stdout_adapter_spec.rb +0 -48
- data/spec/adapters/stdout_json_adapter_spec.rb +0 -62
- data/spec/preprocessors/whitelist_spec.rb +0 -335
- data/spec/utils_spec.rb +0 -84
@@ -1,12 +1,13 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
1
3
|
class Lenjador
|
2
4
|
module Preprocessors
|
3
5
|
class Blacklist
|
4
|
-
|
5
6
|
DEFAULT_ACTION = 'prune'
|
6
7
|
MASK_SYMBOL = '*'
|
7
8
|
MASKED_VALUE = MASK_SYMBOL * 5
|
8
9
|
|
9
|
-
class UnsupportedActionException <
|
10
|
+
class UnsupportedActionException < RuntimeError
|
10
11
|
end
|
11
12
|
|
12
13
|
def initialize(config = {})
|
@@ -19,15 +20,16 @@ class Lenjador
|
|
19
20
|
end
|
20
21
|
|
21
22
|
def process(data)
|
22
|
-
|
23
|
+
case data
|
24
|
+
when Hash
|
23
25
|
data.inject({}) do |mem, (key, val)|
|
24
26
|
if (field = @fields_to_process[key.to_s])
|
25
|
-
|
27
|
+
send(action_method(field[:action]), mem, key, val)
|
26
28
|
else
|
27
29
|
mem.merge(key => process(val))
|
28
30
|
end
|
29
31
|
end
|
30
|
-
|
32
|
+
when Array
|
31
33
|
data.inject([]) do |mem, val|
|
32
34
|
mem + [process(val)]
|
33
35
|
end
|
@@ -43,19 +45,19 @@ class Lenjador
|
|
43
45
|
end
|
44
46
|
|
45
47
|
def validate_action_supported(action)
|
46
|
-
unless
|
47
|
-
raise UnsupportedActionException
|
48
|
+
unless respond_to?(action_method(action).to_sym, true)
|
49
|
+
raise UnsupportedActionException, "Action: #{action} is not supported"
|
48
50
|
end
|
49
51
|
end
|
50
52
|
|
51
|
-
def mask_field(data, key,
|
53
|
+
def mask_field(data, key, _val)
|
52
54
|
data.merge(key => MASKED_VALUE)
|
53
55
|
end
|
54
56
|
|
55
57
|
def prune_field(data, *)
|
56
58
|
data
|
57
59
|
end
|
58
|
-
|
60
|
+
alias exclude_field prune_field
|
59
61
|
end
|
60
62
|
end
|
61
63
|
end
|
@@ -1,10 +1,12 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
1
3
|
require 'lru_redux'
|
2
4
|
|
3
5
|
class Lenjador
|
4
6
|
module Preprocessors
|
5
7
|
class JSONPointerTrie
|
6
|
-
SEPARATOR = '/'
|
7
|
-
WILDCARD = '~'
|
8
|
+
SEPARATOR = '/'
|
9
|
+
WILDCARD = '~'
|
8
10
|
DEFAULT_CACHE_SIZE = 100
|
9
11
|
|
10
12
|
def initialize(cache_size: DEFAULT_CACHE_SIZE, **)
|
@@ -1,3 +1,5 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
1
3
|
require 'lenjador/preprocessors/json_pointer_trie'
|
2
4
|
require 'lenjador/preprocessors/strategies/mask'
|
3
5
|
require 'lenjador/preprocessors/strategies/prune'
|
@@ -6,12 +8,12 @@ class Lenjador
|
|
6
8
|
module Preprocessors
|
7
9
|
class Whitelist
|
8
10
|
DEFAULT_WHITELIST = %w[/id /message /correlation_id /queue].freeze
|
9
|
-
MASK_SYMBOL = '*'
|
11
|
+
MASK_SYMBOL = '*'
|
10
12
|
MASKED_VALUE = MASK_SYMBOL * 5
|
11
13
|
|
12
14
|
PRUNE_ACTION_NAMES = %w[prune exclude].freeze
|
13
15
|
|
14
|
-
class InvalidPointerFormatException <
|
16
|
+
class InvalidPointerFormatException < RuntimeError
|
15
17
|
end
|
16
18
|
|
17
19
|
def initialize(config = {})
|
@@ -31,9 +33,7 @@ class Lenjador
|
|
31
33
|
private
|
32
34
|
|
33
35
|
def validate_pointer(pointer)
|
34
|
-
if pointer.slice(-1) == '/'
|
35
|
-
raise InvalidPointerFormatException, 'Pointer should not contain trailing slash'
|
36
|
-
end
|
36
|
+
raise InvalidPointerFormatException, 'Pointer should not contain trailing slash' if pointer.slice(-1) == '/'
|
37
37
|
end
|
38
38
|
|
39
39
|
def decode(pointer)
|
@@ -45,7 +45,7 @@ class Lenjador
|
|
45
45
|
def build_trie(config)
|
46
46
|
pointers = (config[:pointers] || []) + DEFAULT_WHITELIST
|
47
47
|
|
48
|
-
pointers.reduce(JSONPointerTrie.new(config)) do |trie, pointer|
|
48
|
+
pointers.reduce(JSONPointerTrie.new(**config)) do |trie, pointer|
|
49
49
|
validate_pointer(pointer)
|
50
50
|
|
51
51
|
trie.insert(decode(pointer))
|
data/lib/lenjador/utils.rb
CHANGED
@@ -1,8 +1,17 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
1
3
|
require 'time'
|
4
|
+
require 'oj'
|
2
5
|
|
3
6
|
class Lenjador
|
4
7
|
module Utils
|
5
8
|
DECIMAL_FRACTION_OF_SECOND = 3
|
9
|
+
NO_TRACE_INFORMATION = {}.freeze
|
10
|
+
DUMP_OPTIONS = {
|
11
|
+
mode: :custom,
|
12
|
+
time_format: :xmlschema,
|
13
|
+
second_precision: 3
|
14
|
+
}.freeze
|
6
15
|
|
7
16
|
# Build logstash json compatible event
|
8
17
|
#
|
@@ -13,8 +22,9 @@ class Lenjador
|
|
13
22
|
# @return [Hash]
|
14
23
|
def self.build_event(metadata, level, application_name)
|
15
24
|
overwritable_params
|
16
|
-
.merge(metadata)
|
17
|
-
.merge(
|
25
|
+
.merge!(metadata)
|
26
|
+
.merge!(tracing_information)
|
27
|
+
.merge!(
|
18
28
|
application: application_name,
|
19
29
|
level: level
|
20
30
|
)
|
@@ -34,58 +44,47 @@ class Lenjador
|
|
34
44
|
|
35
45
|
def self.overwritable_params
|
36
46
|
{
|
37
|
-
:@timestamp => Time.now
|
47
|
+
:@timestamp => Time.now
|
38
48
|
}
|
39
49
|
end
|
50
|
+
private_class_method :overwritable_params
|
40
51
|
|
41
|
-
def self.
|
42
|
-
|
43
|
-
object.each do |key, value|
|
44
|
-
object[key] = serialize_time_objects!(value)
|
45
|
-
end
|
46
|
-
elsif object.is_a?(Array)
|
47
|
-
object.each_index do |index|
|
48
|
-
object[index] = serialize_time_objects!(object[index])
|
49
|
-
end
|
50
|
-
elsif object.is_a?(Time) || object.is_a?(Date)
|
51
|
-
object.iso8601
|
52
|
-
else
|
53
|
-
object
|
54
|
-
end
|
55
|
-
end
|
56
|
-
|
57
|
-
if RUBY_PLATFORM =~ /java/
|
58
|
-
require 'jrjackson'
|
59
|
-
|
60
|
-
DUMP_OPTIONS = {
|
61
|
-
timezone: 'utc',
|
62
|
-
date_format: "YYYY-MM-dd'T'HH:mm:ss.SSSX"
|
63
|
-
}.freeze
|
64
|
-
|
65
|
-
def self.generate_json(obj)
|
66
|
-
JrJackson::Json.dump(obj, DUMP_OPTIONS)
|
67
|
-
end
|
68
|
-
else
|
69
|
-
require 'oj'
|
70
|
-
DUMP_OPTIONS = { mode: :compat, time_format: :ruby }.freeze
|
71
|
-
|
72
|
-
def self.generate_json(obj)
|
73
|
-
serialize_time_objects!(obj)
|
74
|
-
|
75
|
-
Oj.dump(obj, DUMP_OPTIONS)
|
76
|
-
end
|
52
|
+
def self.generate_json(obj)
|
53
|
+
Oj.dump(obj, DUMP_OPTIONS)
|
77
54
|
end
|
78
55
|
|
79
56
|
def self.underscore(input)
|
80
57
|
word = input.to_s.dup
|
81
58
|
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!(
|
59
|
+
word.gsub!(/([A-Z]+)([A-Z][a-z])/, '\1_\2')
|
60
|
+
word.gsub!(/([a-z\d])([A-Z])/, '\1_\2')
|
61
|
+
word.tr!('-', '_')
|
85
62
|
word.downcase!
|
86
63
|
word
|
87
64
|
end
|
88
65
|
|
89
|
-
|
66
|
+
# Tracing information
|
67
|
+
#
|
68
|
+
# Tracing information is included only if OpenTracing is defined and if it
|
69
|
+
# supports method called `active_span` (version >= 0.4.1). We use
|
70
|
+
# SpanContext#trace_id and SpanContext#span_id methods to retrieve tracing
|
71
|
+
# information. These methods are not yet supported by the OpenTracing API,
|
72
|
+
# so we first check if these methods exist. Popular tracing libraries
|
73
|
+
# already implement them. These methods are likely to be added to the API
|
74
|
+
# very soon: https://github.com/opentracing/specification/blob/master/rfc/trace_identifiers.md
|
75
|
+
def self.tracing_information
|
76
|
+
return NO_TRACE_INFORMATION if !defined?(OpenTracing) || !OpenTracing.respond_to?(:active_span)
|
77
|
+
|
78
|
+
context = OpenTracing.active_span&.context
|
79
|
+
if context && context.respond_to?(:trace_id) && context.respond_to?(:span_id)
|
80
|
+
{
|
81
|
+
trace_id: context.trace_id,
|
82
|
+
span_id: context.span_id
|
83
|
+
}
|
84
|
+
else
|
85
|
+
NO_TRACE_INFORMATION
|
86
|
+
end
|
87
|
+
end
|
88
|
+
private_class_method :tracing_information
|
90
89
|
end
|
91
90
|
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) }
|
@@ -0,0 +1,48 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
require 'lenjador/adapters/stdout_adapter'
|
3
|
+
|
4
|
+
describe Lenjador::Adapters::StdoutAdapter do
|
5
|
+
it 'creates a stdout logger' do
|
6
|
+
io_logger = described_class.new('service name')
|
7
|
+
|
8
|
+
logger = io_logger.logger
|
9
|
+
expect(logger).to be_a Logger
|
10
|
+
end
|
11
|
+
|
12
|
+
describe '#log' do
|
13
|
+
let(:adapter) { described_class.new('sevice name') }
|
14
|
+
let(:logger) { adapter.logger }
|
15
|
+
|
16
|
+
context 'with only a message' do
|
17
|
+
it 'stringifies it correctly' do
|
18
|
+
expect(logger).to receive(:add).with(Logger::Severity::INFO, 'test')
|
19
|
+
|
20
|
+
adapter.log Lenjador::Severity::INFO, message: 'test'
|
21
|
+
end
|
22
|
+
end
|
23
|
+
|
24
|
+
context 'with an empty message' do
|
25
|
+
it 'stringifies it correctly' do
|
26
|
+
expect(logger).to receive(:add).with(Logger::Severity::INFO, ' {"a":"b"}')
|
27
|
+
|
28
|
+
adapter.log Lenjador::Severity::INFO, message: '', a: 'b'
|
29
|
+
end
|
30
|
+
end
|
31
|
+
|
32
|
+
context 'with no message' do
|
33
|
+
it 'stringifies it correctly' do
|
34
|
+
expect(logger).to receive(:add).with(Logger::Severity::INFO, '{"a":"b"}')
|
35
|
+
|
36
|
+
adapter.log Lenjador::Severity::INFO, a: 'b'
|
37
|
+
end
|
38
|
+
end
|
39
|
+
|
40
|
+
context 'with a message and metadata' do
|
41
|
+
it 'stringifies it correctly' do
|
42
|
+
expect(logger).to receive(:add).with(Logger::Severity::INFO, 'test {"a":"b"}')
|
43
|
+
|
44
|
+
adapter.log Lenjador::Severity::INFO, message: 'test', a: 'b'
|
45
|
+
end
|
46
|
+
end
|
47
|
+
end
|
48
|
+
end
|
@@ -0,0 +1,46 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
require 'json'
|
3
|
+
require 'lenjador/adapters/stdout_json_adapter'
|
4
|
+
|
5
|
+
describe Lenjador::Adapters::StdoutJsonAdapter do
|
6
|
+
let(:stdout) { StringIO.new }
|
7
|
+
|
8
|
+
around do |example|
|
9
|
+
old_stdout = $stdout
|
10
|
+
$stdout = stdout
|
11
|
+
|
12
|
+
begin
|
13
|
+
example.call
|
14
|
+
ensure
|
15
|
+
$stdout = old_stdout
|
16
|
+
end
|
17
|
+
end
|
18
|
+
|
19
|
+
describe '#log' do # rubocop:disable RSpec/MultipleMemoizedHelpers
|
20
|
+
let(:adapter) { described_class.new(service_name) }
|
21
|
+
let(:metadata) { {x: 'y'} }
|
22
|
+
let(:event) { {a: 'b', x: 'y'} }
|
23
|
+
let(:serialized_event) { JSON.dump(event) }
|
24
|
+
let(:service_name) { 'my-service' }
|
25
|
+
let(:application_name) { 'my_service' }
|
26
|
+
let(:info) { Lenjador::Severity::INFO }
|
27
|
+
let(:info_label) { 'info' }
|
28
|
+
|
29
|
+
before do
|
30
|
+
allow(Lenjador::Utils).to receive(:build_event)
|
31
|
+
.with(metadata, info_label, application_name)
|
32
|
+
.and_return(event)
|
33
|
+
end
|
34
|
+
|
35
|
+
it 'sends serialized event to $stdout' do
|
36
|
+
adapter.log(info, metadata)
|
37
|
+
expect(output).to eq("#{serialized_event}\n")
|
38
|
+
end
|
39
|
+
end
|
40
|
+
|
41
|
+
private
|
42
|
+
|
43
|
+
def output
|
44
|
+
stdout.string
|
45
|
+
end
|
46
|
+
end
|
@@ -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
|