lenjador 1.2.1

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.
@@ -0,0 +1,91 @@
1
+ require 'time'
2
+
3
+ class Lenjador
4
+ module Utils
5
+ DECIMAL_FRACTION_OF_SECOND = 3
6
+
7
+ # Build logstash json compatible event
8
+ #
9
+ # @param [Hash] metadata
10
+ # @param [#to_s] level
11
+ # @param [String] service_name
12
+ #
13
+ # @return [Hash]
14
+ def self.build_event(metadata, level, application_name)
15
+ overwritable_params
16
+ .merge(metadata)
17
+ .merge(
18
+ application: application_name,
19
+ level: level
20
+ )
21
+ end
22
+
23
+ # Return application name
24
+ #
25
+ # Returns lower snake case application name. This allows the
26
+ # application value to be used in the elasticsearch index name.
27
+ #
28
+ # @param [String] service_name
29
+ #
30
+ # @return [String]
31
+ def self.application_name(service_name)
32
+ underscore(service_name)
33
+ end
34
+
35
+ def self.overwritable_params
36
+ {
37
+ :@timestamp => Time.now.utc.iso8601(DECIMAL_FRACTION_OF_SECOND)
38
+ }
39
+ end
40
+
41
+ def self.serialize_time_objects!(object)
42
+ if object.is_a?(Hash)
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
77
+ end
78
+
79
+ def self.underscore(input)
80
+ word = input.to_s.dup
81
+ 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!("-", "_")
85
+ word.downcase!
86
+ word
87
+ end
88
+
89
+ private_class_method :overwritable_params
90
+ end
91
+ end
data/lib/lenjador.rb ADDED
@@ -0,0 +1,74 @@
1
+ require 'logger'
2
+ require_relative 'lenjador/adapters'
3
+ require_relative 'lenjador/utils'
4
+ require_relative 'lenjador/null_logger'
5
+ require_relative 'lenjador/preprocessors'
6
+
7
+ LOG_LEVEL_QUERY_METHODS = [:debug?, :info?, :warn?, :error?, :fatal?]
8
+
9
+ class Lenjador
10
+ def self.build(service_name, loggers_config, preprocessors_config = {})
11
+ loggers_config ||= {stdout: nil}
12
+ preprocessors = preprocessors_config.map do |type, arguments|
13
+ Preprocessors.get(type.to_s, arguments || {})
14
+ end
15
+ adapters = loggers_config.map do |type, arguments|
16
+ Adapters.get(type.to_s, service_name, arguments || {})
17
+ end
18
+ new(adapters, preprocessors)
19
+ end
20
+
21
+ def initialize(adapters, preprocessors)
22
+ @adapters = adapters
23
+ @preprocessors = preprocessors
24
+ end
25
+
26
+ def debug(*args, &block)
27
+ log :debug, *args, &block
28
+ end
29
+
30
+ def info(*args, &block)
31
+ log :info, *args, &block
32
+ end
33
+
34
+ def warn(*args, &block)
35
+ log :warn, *args, &block
36
+ end
37
+
38
+ def error(*args, &block)
39
+ log :error, *args, &block
40
+ end
41
+
42
+ def fatal(*args, &block)
43
+ log :fatal, *args, &block
44
+ end
45
+
46
+ LOG_LEVEL_QUERY_METHODS.each do |method|
47
+ define_method(method) do
48
+ @adapters.any? {|adapter| adapter.public_send(method) }
49
+ end
50
+ end
51
+
52
+ private
53
+
54
+ def log(level, *args, &block)
55
+ data = parse_log_data(*args, &block)
56
+ processed_data = preprocess(data)
57
+
58
+ @adapters.each do |adapter|
59
+ adapter.log(level, processed_data)
60
+ end
61
+ end
62
+
63
+ def preprocess(data)
64
+ @preprocessors.inject(data) do |data_to_process, preprocessor|
65
+ preprocessor.process(data_to_process)
66
+ end
67
+ end
68
+
69
+ def parse_log_data(message = nil, metadata = {}, &block)
70
+ return message if message.is_a?(Hash)
71
+
72
+ (metadata || {}).merge(message: block ? block.call : message)
73
+ end
74
+ end
@@ -0,0 +1,48 @@
1
+ require 'spec_helper'
2
+ require_relative '../../lib/lenjador/adapters/stdout_adapter'
3
+
4
+ describe Lenjador::Adapters::StdoutAdapter do
5
+ it 'creates a stdout logger' do
6
+ io_logger = described_class.new(0)
7
+
8
+ logger = io_logger.instance_variable_get(:@logger)
9
+ expect(logger).to be_a Logger
10
+ end
11
+
12
+ describe '#log' do
13
+ let(:adapter) { described_class.new(0) }
14
+ let(:logger) { adapter.logger }
15
+
16
+ context 'with only a message' do
17
+ it 'stringifies it correctly' do
18
+ expect(logger).to receive(:info).with('test')
19
+
20
+ adapter.log :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(:info).with(' {"a":"b"}')
27
+
28
+ adapter.log :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(:info).with('{"a":"b"}')
35
+
36
+ adapter.log :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(:info).with('test {"a":"b"}')
43
+
44
+ adapter.log :info, message: 'test', a: 'b'
45
+ end
46
+ end
47
+ end
48
+ end
@@ -0,0 +1,62 @@
1
+ require 'spec_helper'
2
+ require 'json'
3
+ require_relative '../../lib/lenjador/adapters/stdout_json_adapter'
4
+
5
+ describe Lenjador::Adapters::StdoutJsonAdapter do
6
+ let(:debug_level_code) { 0 }
7
+ let(:debug_level) { Lenjador::Adapters::LOG_LEVELS[debug_level_code] }
8
+ let(:info_level_code) { 1 }
9
+ let(:info_level) { Lenjador::Adapters::LOG_LEVELS[info_level_code] }
10
+
11
+ let(:stdout) { StringIO.new }
12
+
13
+ around do |example|
14
+ old_stdout = $stdout
15
+ $stdout = stdout
16
+
17
+ begin
18
+ example.call
19
+ ensure
20
+ $stdout = old_stdout
21
+ end
22
+ end
23
+
24
+ describe '#log' do
25
+ context 'when below threshold' do
26
+ let(:adapter) { described_class.new(debug_level_code, service_name) }
27
+ let(:metadata) { {x: 'y'} }
28
+ let(:event) { {a: 'b', x: 'y'} }
29
+ let(:serialized_event) { JSON.dump(event) }
30
+ let(:service_name) { 'my-service' }
31
+ let(:application_name) { 'my_service' }
32
+
33
+ before do
34
+ allow(Lenjador::Utils).to receive(:build_event)
35
+ .with(metadata, info_level, application_name)
36
+ .and_return(event)
37
+ end
38
+
39
+ it 'sends serialized event to $stdout' do
40
+ adapter.log(info_level, metadata)
41
+ expect(output).to eq serialized_event + "\n"
42
+ end
43
+ end
44
+
45
+ context 'when above threshold' do
46
+ let(:adapter) { described_class.new(info_level_code, service_name) }
47
+ let(:metadata) { {x: 'y'} }
48
+ let(:service_name) { 'my-service' }
49
+
50
+ it 'does not log the event' do
51
+ adapter.log(debug_level, metadata)
52
+ expect(output).to be_empty
53
+ end
54
+ end
55
+ end
56
+
57
+ private
58
+
59
+ def output
60
+ stdout.string
61
+ end
62
+ end
@@ -0,0 +1,142 @@
1
+ require 'spec_helper'
2
+
3
+ describe Lenjador do
4
+ describe '.build' do
5
+ it 'creates stdout logger' do
6
+ expect(described_class).to receive(:new) do |adapters|
7
+ expect(adapters.count).to be(1)
8
+ expect(adapters.first).to be_a(described_class::Adapters::StdoutAdapter)
9
+ end
10
+
11
+ described_class.build('test_service', stdout: nil)
12
+ end
13
+
14
+ it 'creates stdout json logger' do
15
+ expect(described_class).to receive(:new) do |adapters|
16
+ expect(adapters.count).to be(1)
17
+ expect(adapters.first).to be_a(described_class::Adapters::StdoutJsonAdapter)
18
+ end
19
+
20
+ described_class.build('test_service', stdout: {json: true})
21
+ end
22
+
23
+ it 'creates stdout logger when no loggers are specified' do
24
+ expect(described_class).to receive(:new) do |adapters|
25
+ expect(adapters.count).to be(1)
26
+ expect(adapters.first).to be_a(described_class::Adapters::StdoutAdapter)
27
+ end
28
+
29
+ described_class.build('test_service', nil)
30
+ end
31
+
32
+ it 'creates preprocessor when preprocessor defined' do
33
+ expect(described_class).to receive(:new) do |adapters, preprocessors|
34
+ expect(preprocessors.count).to be(1)
35
+ expect(preprocessors.first).to be_a(described_class::Preprocessors::Blacklist)
36
+ end
37
+
38
+ preprocessors = {blacklist: {fields: []}}
39
+ described_class.build('test_service', nil, preprocessors)
40
+ end
41
+ end
42
+
43
+ context 'when preprocessor defined' do
44
+ let(:lenjador) { described_class.new([adapter], [preprocessor]) }
45
+ let(:adapter) { double }
46
+ let(:preprocessor) { double }
47
+ let(:data) { {data: 'data'} }
48
+
49
+ it 'preprocesses data before logging' do
50
+ expect(preprocessor).to receive(:process).with(data).and_return(data.merge(processed: true)).ordered
51
+ expect(adapter).to receive(:log).with(:info, data.merge(processed: true)).ordered
52
+
53
+ lenjador.info(data)
54
+ end
55
+ end
56
+
57
+ context 'when parsing log data' do
58
+ let(:lenjador) { described_class.new([adapter], preprocessors) }
59
+ let(:adapter) { double }
60
+ let(:preprocessors) { [] }
61
+
62
+ it 'parses empty string with nil metadata' do
63
+ expect(adapter).to receive(:log).with(:info, message: '')
64
+
65
+ lenjador.info('', nil)
66
+ end
67
+
68
+ it 'parses nil as metadata' do
69
+ expect(adapter).to receive(:log).with(:info, message: nil)
70
+
71
+ lenjador.info(nil)
72
+ end
73
+
74
+ it 'parses only message' do
75
+ expect(adapter).to receive(:log).with(:info, message: 'test message')
76
+
77
+ lenjador.info 'test message'
78
+ end
79
+
80
+ it 'parses only metadata' do
81
+ expect(adapter).to receive(:log).with(:info, test: 'data')
82
+
83
+ lenjador.info test: 'data'
84
+ end
85
+
86
+ it 'parses message and metadata' do
87
+ expect(adapter).to receive(:log).with(:info, message: 'test message', test: 'data')
88
+
89
+ lenjador.info 'test message', test: 'data'
90
+ end
91
+
92
+ it 'parses block as a message' do
93
+ message = 'test message'
94
+ expect(adapter).to receive(:log).with(:info, message: message)
95
+
96
+ lenjador.info { message }
97
+ end
98
+
99
+ it 'ignores progname on block syntax' do
100
+ message = 'test message'
101
+ expect(adapter).to receive(:log).with(:info, message: message)
102
+
103
+ lenjador.info('progname') { message }
104
+ end
105
+ end
106
+
107
+ context 'log level queries' do
108
+ context 'when adapter has debug level' do
109
+ let(:logger) do
110
+ described_class.build('test_service', stdout: {level: 'debug'})
111
+ end
112
+
113
+ it 'responds true to debug? and higher levels' do
114
+ expect(logger.debug?).to be(true)
115
+ expect(logger.info?).to be(true)
116
+ expect(logger.warn?).to be(true)
117
+ expect(logger.error?).to be(true)
118
+ expect(logger.fatal?).to be(true)
119
+ end
120
+ end
121
+
122
+ context 'when adapter has info level' do
123
+ let(:logger) do
124
+ described_class.build('test_service', stdout: {level: 'info'})
125
+ end
126
+
127
+ it 'responds true to info? and higher levels' do
128
+ expect(logger.debug?).to be(false)
129
+ expect(logger.info?).to be(true)
130
+ expect(logger.warn?).to be(true)
131
+ expect(logger.error?).to be(true)
132
+ expect(logger.fatal?).to be(true)
133
+ end
134
+ end
135
+ end
136
+
137
+ it 'has the same interface as Ruby logger' do
138
+ skip "https://salemove.atlassian.net/browse/INF-464"
139
+ logger = described_class.build('test_service', stdout: {level: 'debug'})
140
+ expect(logger).to implement_interface(Logger)
141
+ end
142
+ end
@@ -0,0 +1,131 @@
1
+ require 'spec_helper'
2
+ require_relative '../../lib/lenjador/preprocessors/blacklist'
3
+
4
+ describe Lenjador::Preprocessors::Blacklist do
5
+ subject(:processed_data) { described_class.new(config).process(data) }
6
+
7
+ let(:config) {{
8
+ fields: [{ key: 'field', action: action }]
9
+ }}
10
+
11
+ let(:action) {}
12
+ let(:data) {{
13
+ field: value,
14
+ data: {
15
+ field: 'secret'
16
+ },
17
+ array: [{field: 'secret'}]
18
+ }}
19
+
20
+ let(:value) { 'secret' }
21
+
22
+ context 'when action is unsupported' do
23
+ let(:action) { 'reverse' }
24
+
25
+ it 'throws exception' do
26
+ expect { processed_data }.to raise_exception(Lenjador::Preprocessors::Blacklist::UnsupportedActionException)
27
+ end
28
+ end
29
+
30
+ context 'when action is "exclude"' do
31
+ let(:action) { 'exclude' }
32
+
33
+ it 'removes the field' do
34
+ expect(processed_data).not_to include(:field)
35
+ end
36
+
37
+ it 'removes nested field' do
38
+ expect(processed_data).not_to include_at_depth({field: 'secret'}, 1)
39
+ end
40
+
41
+ it 'removes nested in array field' do
42
+ expect(processed_data[:array]).not_to include({field: 'secret'})
43
+ end
44
+
45
+ context 'when field is deeply nested' do
46
+ let(:depth) { 10 }
47
+ let(:data) { data_with_nested_field({field: 'secret'}, depth) }
48
+
49
+ it 'removes deeply nested field' do
50
+ expect(processed_data).not_to include_at_depth({field: 'secret'}, depth)
51
+ end
52
+ end
53
+ end
54
+
55
+ context 'when action is "mask"' do
56
+ let(:action) { 'mask' }
57
+
58
+ it 'masks nested field' do
59
+ expect(processed_data).to include_at_depth({field: '*****'}, 1)
60
+ end
61
+
62
+ it 'masks nested in array field' do
63
+ expect(processed_data[:array]).to include({field: '*****'})
64
+ end
65
+
66
+ context 'when field is string' do
67
+ let(:value) { 'secret' }
68
+
69
+ it 'masks value with asterisks' do
70
+ expect(processed_data).to include(field: '*****')
71
+ end
72
+ end
73
+
74
+ context 'when field is number' do
75
+ let(:value) { 42 }
76
+
77
+ it 'masks number value' do
78
+ expect(processed_data).to include(field: '*****')
79
+ end
80
+ end
81
+
82
+ context 'when field is boolean' do
83
+ let(:value) { true }
84
+
85
+ it 'masks value with asterisks' do
86
+ expect(processed_data).to include(field: '*****')
87
+ end
88
+ end
89
+
90
+ context 'when field is array' do
91
+ let(:value) { [1,2,3,4] }
92
+
93
+ it 'masks value with asterisks' do
94
+ expect(processed_data).to include(field: '*****')
95
+ end
96
+ end
97
+
98
+ context 'when field is hash' do
99
+ let(:value) { {data: {}} }
100
+
101
+ it 'masks value with asterisks' do
102
+ expect(processed_data).to include(field: '*****')
103
+ end
104
+ end
105
+
106
+ context 'when field is deeply nested' do
107
+ let(:depth) { 10 }
108
+ let(:data) { data_with_nested_field({field: 'secret'}, depth) }
109
+
110
+ it 'masks deeply nested field' do
111
+ expect(processed_data).to include_at_depth({field: '*****'}, depth)
112
+ end
113
+ end
114
+ end
115
+
116
+ def data_with_nested_field(field, depth)
117
+ depth.times.inject(field) do |mem|
118
+ {}.merge(data: mem)
119
+ end
120
+ end
121
+
122
+ RSpec::Matchers.define :include_at_depth do |expected_hash, depth|
123
+ match do |actual|
124
+ nested_data = depth.times.inject(actual) do |mem|
125
+ mem[:data]
126
+ end
127
+
128
+ expect(nested_data).to include(expected_hash)
129
+ end
130
+ end
131
+ end
@@ -0,0 +1,36 @@
1
+ require 'spec_helper'
2
+ require 'lenjador/preprocessors/json_pointer_trie'
3
+
4
+ RSpec.describe Lenjador::Preprocessors::JSONPointerTrie do
5
+ let(:trie) { described_class.new }
6
+
7
+ describe '#includes?' do
8
+ it 'returns true for empty prefix' do
9
+ expect(trie).to include('')
10
+ end
11
+
12
+ it 'returns true if trie contains requested prefix or value itself' do
13
+ trie.insert('/data/nested/key')
14
+
15
+ expect(trie).to include('/data')
16
+ expect(trie).to include('/data/nested')
17
+ expect(trie).to include('/data/nested/key')
18
+ end
19
+
20
+ it 'returns false if trie does not contain requested prefix or value' do
21
+ trie.insert('/data/nested/key')
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')
26
+ end
27
+
28
+ it 'returns true if trie contains requested prefix under wildcard' do
29
+ trie.insert('/data/~/key')
30
+
31
+ expect(trie).to include('/data/arbitrary_key/key')
32
+ expect(trie).to include('/data/another_key/key')
33
+ expect(trie).to_not include('/data/arbitrary_key/bad_nested_key')
34
+ end
35
+ end
36
+ end