logasm-jruby 1.2.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,40 @@
1
+ require 'lru_redux'
2
+
3
+ class Logasm
4
+ module Preprocessors
5
+ class JSONPointerTrie
6
+ SEPARATOR = '/'.freeze
7
+ WILDCARD = '~'.freeze
8
+ DEFAULT_CACHE_SIZE = 100
9
+
10
+ def initialize(cache_size: DEFAULT_CACHE_SIZE, **)
11
+ @root_node = {}
12
+ @cache = LruRedux::Cache.new(cache_size)
13
+ end
14
+
15
+ def insert(pointer)
16
+ split_path(pointer).reduce(@root_node) do |tree, key|
17
+ tree[key] ||= {}
18
+ end
19
+
20
+ self
21
+ end
22
+
23
+ def include?(path)
24
+ @cache.getset(path) { traverse_path(path) }
25
+ end
26
+
27
+ private
28
+
29
+ def traverse_path(path)
30
+ split_path(path).reduce(@root_node) do |node, key|
31
+ node[key] || node[WILDCARD] || (break false)
32
+ end
33
+ end
34
+
35
+ def split_path(path)
36
+ path.split(SEPARATOR).reject(&:empty?)
37
+ end
38
+ end
39
+ end
40
+ end
@@ -0,0 +1,43 @@
1
+ class Logasm
2
+ module Preprocessors
3
+ module Strategies
4
+ class Mask
5
+ MASK_SYMBOL = '*'.freeze
6
+ MASKED_VALUE = MASK_SYMBOL * 5
7
+
8
+ def initialize(trie)
9
+ @trie = trie
10
+ end
11
+
12
+ def process(data, pointer = '')
13
+ return MASKED_VALUE unless @trie.include?(pointer)
14
+
15
+ case data
16
+ when Hash
17
+ process_hash(data, pointer)
18
+
19
+ when Array
20
+ process_array(data, pointer)
21
+
22
+ else
23
+ data
24
+ end
25
+ end
26
+
27
+ private
28
+
29
+ def process_hash(data, parent_pointer)
30
+ data.each_with_object({}) do |(key, value), result|
31
+ result[key] = process(value, "#{parent_pointer}/#{key}")
32
+ end
33
+ end
34
+
35
+ def process_array(data, parent_pointer)
36
+ data.each_with_index.map do |value, index|
37
+ process(value, "#{parent_pointer}/#{index}")
38
+ end
39
+ end
40
+ end
41
+ end
42
+ end
43
+ end
@@ -0,0 +1,44 @@
1
+ class Logasm
2
+ module Preprocessors
3
+ module Strategies
4
+ class Prune
5
+ def initialize(trie)
6
+ @trie = trie
7
+ end
8
+
9
+ def process(data, pointer = '')
10
+ return nil unless @trie.include?(pointer)
11
+
12
+ case data
13
+ when Hash
14
+ process_hash(data, pointer)
15
+
16
+ when Array
17
+ process_array(data, pointer)
18
+
19
+ else
20
+ data
21
+ end
22
+ end
23
+
24
+ private
25
+
26
+ def process_hash(data, parent_pointer)
27
+ data.each_with_object({}) do |(key, value), result|
28
+ path = "#{parent_pointer}/#{key}"
29
+
30
+ result[key] = process(value, path) if @trie.include?(path)
31
+ end
32
+ end
33
+
34
+ def process_array(data, parent_pointer)
35
+ data.each_with_index.each_with_object([]) do |(value, index), result|
36
+ path = "#{parent_pointer}/#{index}"
37
+
38
+ result << process(value, path) if @trie.include?(path)
39
+ end
40
+ end
41
+ end
42
+ end
43
+ end
44
+ end
@@ -0,0 +1,56 @@
1
+ require 'logasm/preprocessors/json_pointer_trie'
2
+ require 'logasm/preprocessors/strategies/mask'
3
+ require 'logasm/preprocessors/strategies/prune'
4
+
5
+ class Logasm
6
+ module Preprocessors
7
+ class Whitelist
8
+ DEFAULT_WHITELIST = %w[/id /message /correlation_id /queue].freeze
9
+ MASK_SYMBOL = '*'.freeze
10
+ MASKED_VALUE = MASK_SYMBOL * 5
11
+
12
+ PRUNE_ACTION_NAMES = %w[prune exclude].freeze
13
+
14
+ class InvalidPointerFormatException < Exception
15
+ end
16
+
17
+ def initialize(config = {})
18
+ trie = build_trie(config)
19
+
20
+ @strategy = if PRUNE_ACTION_NAMES.include?(config[:action].to_s)
21
+ Strategies::Prune.new(trie)
22
+ else
23
+ Strategies::Mask.new(trie)
24
+ end
25
+ end
26
+
27
+ def process(data)
28
+ @strategy.process(data)
29
+ end
30
+
31
+ private
32
+
33
+ def validate_pointer(pointer)
34
+ if pointer.slice(-1) == '/'
35
+ raise InvalidPointerFormatException, 'Pointer should not contain trailing slash'
36
+ end
37
+ end
38
+
39
+ def decode(pointer)
40
+ pointer
41
+ .gsub('~1', '/')
42
+ .gsub('~0', '~')
43
+ end
44
+
45
+ def build_trie(config)
46
+ pointers = (config[:pointers] || []) + DEFAULT_WHITELIST
47
+
48
+ pointers.reduce(JSONPointerTrie.new(config)) do |trie, pointer|
49
+ validate_pointer(pointer)
50
+
51
+ trie.insert(decode(pointer))
52
+ end
53
+ end
54
+ end
55
+ end
56
+ end
@@ -0,0 +1,91 @@
1
+ require 'time'
2
+
3
+ class Logasm
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/logasm.gemspec ADDED
@@ -0,0 +1,36 @@
1
+ # coding: utf-8
2
+ lib = File.expand_path('../lib', __FILE__)
3
+ $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
4
+
5
+ Gem::Specification.new do |gem|
6
+ if RUBY_PLATFORM =~ /java/
7
+ gem.name = 'logasm-jruby'
8
+ else
9
+ gem.name = 'logasm'
10
+ end
11
+
12
+ gem.version = '1.2.0'
13
+ gem.authors = ["Salemove"]
14
+ gem.email = ["support@salemove.com"]
15
+ gem.description = %q{It's logasmic}
16
+ gem.summary = %q{What description said}
17
+ gem.license = "MIT"
18
+
19
+ gem.files = `git ls-files`.split($/)
20
+ gem.executables = gem.files.grep(%r{^bin/}) { |f| File.basename(f) }
21
+ gem.test_files = gem.files.grep(%r{^(test|spec|features)/})
22
+ gem.require_paths = ["lib"]
23
+
24
+ gem.add_dependency 'lru_redux'
25
+
26
+ if RUBY_PLATFORM =~ /java/
27
+ gem.add_dependency 'jrjackson'
28
+ else
29
+ gem.add_dependency 'oj'
30
+ end
31
+
32
+ gem.add_development_dependency "bundler", "~> 1.3"
33
+ gem.add_development_dependency "rake"
34
+ gem.add_development_dependency "bunny"
35
+ gem.add_development_dependency "benchmark-ips"
36
+ end
@@ -0,0 +1,48 @@
1
+ require 'spec_helper'
2
+ require_relative '../../lib/logasm/adapters/stdout_adapter'
3
+
4
+ describe Logasm::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,43 @@
1
+ require 'spec_helper'
2
+ require 'json'
3
+ require_relative '../../lib/logasm/adapters/stdout_json_adapter'
4
+
5
+ describe Logasm::Adapters::StdoutJsonAdapter do
6
+ let(:debug_level_code) { 0 }
7
+ let(:debug_level) { Logasm::Adapters::LOG_LEVELS[debug_level_code] }
8
+ let(:info_level_code) { 1 }
9
+ let(:info_level) { Logasm::Adapters::LOG_LEVELS[info_level_code] }
10
+
11
+ describe '#log' do
12
+ context 'when below threshold' do
13
+ let(:adapter) { described_class.new(debug_level_code, service_name) }
14
+ let(:metadata) { {x: 'y'} }
15
+ let(:event) { {a: 'b', x: 'y'} }
16
+ let(:serialized_event) { JSON.dump(event) }
17
+ let(:service_name) { 'my-service' }
18
+ let(:application_name) { 'my_service' }
19
+
20
+ before do
21
+ allow(Logasm::Utils).to receive(:build_event)
22
+ .with(metadata, info_level, application_name)
23
+ .and_return(event)
24
+ end
25
+
26
+ it 'sends serialized event to STDOUT' do
27
+ expect(STDOUT).to receive(:puts).with(serialized_event)
28
+ adapter.log(info_level, metadata)
29
+ end
30
+ end
31
+
32
+ context 'when above threshold' do
33
+ let(:adapter) { described_class.new(info_level_code, service_name) }
34
+ let(:metadata) { {x: 'y'} }
35
+ let(:service_name) { 'my-service' }
36
+
37
+ it 'does not log the event' do
38
+ expect(STDOUT).to_not receive(:puts)
39
+ adapter.log(debug_level, metadata)
40
+ end
41
+ end
42
+ end
43
+ end
@@ -0,0 +1,142 @@
1
+ require 'spec_helper'
2
+
3
+ describe Logasm 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(:logasm) { 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
+ logasm.info(data)
54
+ end
55
+ end
56
+
57
+ context 'when parsing log data' do
58
+ let(:logasm) { 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
+ logasm.info('', nil)
66
+ end
67
+
68
+ it 'parses nil as metadata' do
69
+ expect(adapter).to receive(:log).with(:info, message: nil)
70
+
71
+ logasm.info(nil)
72
+ end
73
+
74
+ it 'parses only message' do
75
+ expect(adapter).to receive(:log).with(:info, message: 'test message')
76
+
77
+ logasm.info 'test message'
78
+ end
79
+
80
+ it 'parses only metadata' do
81
+ expect(adapter).to receive(:log).with(:info, test: 'data')
82
+
83
+ logasm.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
+ logasm.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
+ logasm.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
+ logasm.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