lenjador 1.2.1 → 2.1.0

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