lenjador 1.2.1

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