lenjador 1.2.1 → 2.1.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.
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  class Lenjador
2
4
  module Preprocessors
3
5
  def self.get(type, arguments)
@@ -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 < Exception
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
- if data.is_a? Hash
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
- self.send(action_method(field[:action]), mem, key, val)
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
- elsif data.is_a? Array
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 self.respond_to?(action_method(action).to_sym, true)
47
- raise UnsupportedActionException.new("Action: #{action} is not supported")
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, val)
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
- alias_method :exclude_field, :prune_field
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 = '/'.freeze
7
- WILDCARD = '~'.freeze
8
+ SEPARATOR = '/'
9
+ WILDCARD = '~'
8
10
  DEFAULT_CACHE_SIZE = 100
9
11
 
10
12
  def initialize(cache_size: DEFAULT_CACHE_SIZE, **)
@@ -1,8 +1,10 @@
1
+ # frozen_string_literal: true
2
+
1
3
  class Lenjador
2
4
  module Preprocessors
3
5
  module Strategies
4
6
  class Mask
5
- MASK_SYMBOL = '*'.freeze
7
+ MASK_SYMBOL = '*'
6
8
  MASKED_VALUE = MASK_SYMBOL * 5
7
9
 
8
10
  def initialize(trie)
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  class Lenjador
2
4
  module Preprocessors
3
5
  module Strategies
@@ -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 = '*'.freeze
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 < Exception
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))
@@ -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.utc.iso8601(DECIMAL_FRACTION_OF_SECOND)
47
+ :@timestamp => Time.now
38
48
  }
39
49
  end
50
+ private_class_method :overwritable_params
40
51
 
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
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
- private_class_method :overwritable_params
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 '../../lib/lenjador/preprocessors/blacklist'
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
- fields: [{ key: 'field', action: action }]
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
- field: value,
14
- data: {
15
- field: 'secret'
16
- },
17
- array: [{field: 'secret'}]
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({field: 'secret'})
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({field: '*****'})
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: '*****')
@@ -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).to_not include('/bad_data')
24
- expect(trie).to_not include('/data/bad_nested')
25
- expect(trie).to_not include('/data/nested/bad_key')
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).to_not include('/data/arbitrary_key/bad_nested_key')
33
+ expect(trie).not_to include('/data/arbitrary_key/bad_nested_key')
34
34
  end
35
35
  end
36
36
  end