logasm-jruby 1.2.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.
@@ -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