lenjador 1.2.1 → 2.1.0
Sign up to get free protection for your applications and to get access to all the features.
- 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
|