lenjador 1.2.1

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.
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA1:
3
+ metadata.gz: 0b22b9348f98f915eaa67dec7cbbe762d09f86dc
4
+ data.tar.gz: 2e22ff6de0251d2e007b83e626e14c20d860b281
5
+ SHA512:
6
+ metadata.gz: e48ff7741f76c07e5dd00e910e32cd6013ee4ff6854ccfc29bf4dc249e54635500c4ff5fedd51ac7394a643baa4a6e1a60269b41b9c5fb2006110fefe45f13af
7
+ data.tar.gz: 0cd79d4f685890c86d4c0c182f256f35c0dbe48a95b77a548193cbec05f21a8df5ce551c1fd77fe5a9a751a5a9491b04c24e78392c9bb7ae7fa92ea2f272ae29
data/.gitignore ADDED
@@ -0,0 +1,4 @@
1
+ node_modules/
2
+ npm-debug.log
3
+ Gemfile.lock
4
+
data/.rspec ADDED
@@ -0,0 +1,2 @@
1
+ --color
2
+ --format progress
data/.ruby-gemset ADDED
@@ -0,0 +1 @@
1
+ logasm
data/.ruby-version ADDED
@@ -0,0 +1 @@
1
+ ruby-2.4.1
data/.travis.yml ADDED
@@ -0,0 +1,7 @@
1
+ language: ruby
2
+ rvm:
3
+ - 2.2
4
+ - 2.3
5
+ - 2.4
6
+ - 2.5
7
+ - jruby
data/Gemfile ADDED
@@ -0,0 +1,8 @@
1
+ source "https://rubygems.org"
2
+
3
+ group :test, :development do
4
+ gem 'pry'
5
+ gem 'rspec'
6
+ end
7
+
8
+ gemspec
data/README.md ADDED
@@ -0,0 +1,121 @@
1
+ Lenjador
2
+ ================
3
+
4
+ ## Usage
5
+
6
+ ### Creating a new Lenjador logger in Ruby
7
+
8
+ ```ruby
9
+ Lenjador.build(application_name, logger_config)
10
+ ```
11
+
12
+ <b>logger_config</b> is a hash with logger types and their configuration.
13
+
14
+ #### Configuration
15
+
16
+ ```
17
+ loggers:
18
+ stdout:
19
+ level: 'debug'
20
+ ```
21
+ Supported log levels:
22
+
23
+ 1. fatal
24
+ 2. error
25
+ 3. warn
26
+ 4. info
27
+ 5. debug
28
+
29
+ For example level: 'warn' will log everything with warn and above.
30
+
31
+ #### Examples
32
+
33
+ Creating a new stdout logger
34
+
35
+ ```ruby
36
+ require 'lenjador'
37
+
38
+ lenjador = Lenjador.build('myApp', stdout: nil)
39
+ ```
40
+
41
+ When no loggers are specified, it creates a stdout logger by default.
42
+
43
+ ## Preprocessors
44
+
45
+ Preprocessors allow modification of log messages, prior to sending of the message to the configured logger(s).
46
+
47
+ ### Blacklist
48
+
49
+ Excludes or masks defined fields of the passed hash object.
50
+ You can specify the name of the field and which action to take on it.
51
+ Nested hashes of any level are preprocessed as well.
52
+
53
+ Available actions:
54
+
55
+ * `prune` (default) - fully excludes the field and its value from the hash.
56
+ * `mask` - replaces every character from the original value with `*`.
57
+ In case of `array`, `hash` or `boolean` value is replaced with one `*`.
58
+
59
+ #### Configuration
60
+
61
+ ```yaml
62
+ preprocessors:
63
+ blacklist:
64
+ fields:
65
+ - key: password
66
+ - key: phone
67
+ action: mask
68
+ ```
69
+
70
+ #### Usage
71
+
72
+ ```ruby
73
+ logger = Lenjador.build(application_name, logger_config, preprocessors)
74
+
75
+ input = {password: 'password', info: {phone: '+12055555555'}}
76
+
77
+ logger.debug("Received request", input)
78
+ ```
79
+
80
+ Logger output:
81
+
82
+ ```
83
+ Received request {"info":{"phone":"************"}}
84
+ ```
85
+
86
+ ### Whitelist
87
+
88
+ Prunes or masks all the fields except those whitelisted in the configuration using [JSON Pointer](https://tools.ietf.org/html/rfc6901).
89
+ Only simple values(`string`, `number`, `boolean`) can be whitelisted.
90
+ Whitelisting array and hash elements can be done using wildcard symbol `~`.
91
+
92
+ Available actions:
93
+
94
+ * `mask` (default) - replaces every character from the original value with `*`.
95
+ In case of `array`, `hash` or `boolean` value is replaced with one `*`.
96
+ * `prune` - fully excludes the field and its value from the hash.
97
+
98
+ #### Configuration
99
+
100
+ ```yaml
101
+ preprocessors:
102
+ whitelist:
103
+ pointers: ['/info/phone', '/addresses/~/host']
104
+ action: prune
105
+ ```
106
+
107
+ #### Usage
108
+
109
+ ```ruby
110
+ logger = Lenjador.build(application_name, logger_config, preprocessors)
111
+
112
+ input = {password: 'password', info: {phone: '+12055555555'}, addresses: [{host: 'example.com', path: 'info'}]}
113
+
114
+ logger.debug("Received request", input)
115
+ ```
116
+
117
+ Logger output:
118
+
119
+ ```
120
+ Received request {password: "********", "info": {"phone": "+12055555555"}, "addresses": [{"host": "example.com","path": "****"}]}
121
+ ```
data/Rakefile ADDED
@@ -0,0 +1,7 @@
1
+ require "bundler/gem_tasks"
2
+
3
+ require "rspec/core/rake_task"
4
+ RSpec::Core::RakeTask.new(:spec)
5
+
6
+ task ci: :spec
7
+ task default: :spec
@@ -0,0 +1,53 @@
1
+ require 'bundler/setup'
2
+ require 'lenjador/preprocessors/whitelist'
3
+ require 'benchmark/ips'
4
+
5
+ pointers = %w[
6
+ /scalar
7
+ /flat_hash/~
8
+ /nested_hash/~/deep_hash/~
9
+ /flat_array/~
10
+ /nested_array/~/deep_array/~
11
+ ]
12
+
13
+ %w[prune mask].each do |action|
14
+ preprocessor = Lenjador::Preprocessors::Whitelist.new(pointers: pointers, action: action)
15
+
16
+ Benchmark.ips do |x|
17
+ x.config(time: 5, warmup: 2)
18
+
19
+ x.report("Scalar value whitelisting (action=#{action})") do
20
+ preprocessor.process(scalar: 'value', bad_scalar: 'value', hash: {})
21
+ end
22
+
23
+ x.report("Flat hash whitelisting (action=#{action})") do
24
+ preprocessor.process(flat_hash: { scalar: 'value', array: [1, 2], hash: {} })
25
+ end
26
+
27
+ x.report("Nested hash whitelisting (action=#{action})") do
28
+ preprocessor.process(
29
+ nested_hash: {
30
+ next_level_hash: {
31
+ deep_hash: { scalar: 'value', array: [1, 2] }
32
+ },
33
+ next_level_hash2: {
34
+ deep_hash: { scalar: 'value', array: [1, 2] }
35
+ },
36
+ next_level_hash3: {
37
+ deep_hash: { scalar: 'value', array: [1, 2] }
38
+ }
39
+ }
40
+ )
41
+ end
42
+
43
+ x.report("Flat array whitelisting (action=#{action})") do
44
+ preprocessor.process(
45
+ nested_array: [
46
+ { deep_array: [1, 2, 3] },
47
+ { deep_array: [1, 2, 3] },
48
+ { deep_array: [1, 2, 3] }
49
+ ]
50
+ )
51
+ end
52
+ end
53
+ end
data/lenjador.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 = 'lenjador-jruby'
8
+ else
9
+ gem.name = 'lenjador'
10
+ end
11
+
12
+ gem.version = '1.2.1'
13
+ gem.authors = ["Salemove"]
14
+ gem.email = ["support@salemove.com"]
15
+ gem.description = %q{It's lenjadoric}
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,29 @@
1
+ require 'forwardable'
2
+
3
+ class Lenjador
4
+ module Adapters
5
+ class StdoutAdapter
6
+ extend Forwardable
7
+
8
+ attr_reader :logger
9
+
10
+ def_delegators :@logger, :debug?, :info?, :warn?, :error?, :fatal?
11
+
12
+ def initialize(level, *)
13
+ @logger = Logger.new(STDOUT)
14
+ @logger.level = level
15
+ end
16
+
17
+ def log(level, metadata = {})
18
+ message = metadata[:message]
19
+ data = metadata.select { |key, value| key != :message }
20
+ log_data = [
21
+ message,
22
+ data.empty? ? nil : Utils.generate_json(data)
23
+ ].compact.join(' ')
24
+
25
+ @logger.public_send level, log_data
26
+ end
27
+ end
28
+ end
29
+ end
@@ -0,0 +1,60 @@
1
+ class Lenjador
2
+ module Adapters
3
+ class StdoutJsonAdapter
4
+ def initialize(level, service_name, *)
5
+ @level = level
6
+ @service_name = service_name
7
+ @application_name = Utils.application_name(service_name)
8
+ @mutex = Mutex.new if RUBY_ENGINE == "jruby"
9
+ end
10
+
11
+ def log(level, metadata = {})
12
+ if meets_threshold?(level)
13
+ message = Utils.build_event(metadata, level, @application_name)
14
+ print_line(Utils.generate_json(message))
15
+ end
16
+ end
17
+
18
+ def debug?
19
+ meets_threshold?(:debug)
20
+ end
21
+
22
+ def info?
23
+ meets_threshold?(:info)
24
+ end
25
+
26
+ def warn?
27
+ meets_threshold?(:warn)
28
+ end
29
+
30
+ def error?
31
+ meets_threshold?(:error)
32
+ end
33
+
34
+ def fatal?
35
+ meets_threshold?(:fatal)
36
+ end
37
+
38
+ private
39
+
40
+ def meets_threshold?(level)
41
+ LOG_LEVELS.index(level.to_s) >= @level
42
+ end
43
+
44
+ # puts is atomic in MRI starting from 2.5.0
45
+ if RUBY_ENGINE == "ruby" && RUBY_VERSION >= "2.5.0"
46
+ def print_line(str)
47
+ $stdout.puts(str)
48
+ end
49
+ elsif RUBY_ENGINE == "jruby"
50
+ def print_line(str)
51
+ @mutex.synchronize { $stdout.write(str + "\n") }
52
+ end
53
+ else
54
+ def print_line(str)
55
+ $stdout.write(str + "\n")
56
+ end
57
+ end
58
+ end
59
+ end
60
+ end
@@ -0,0 +1,22 @@
1
+ class Lenjador
2
+ module Adapters
3
+ LOG_LEVELS = %w(debug info warn error fatal unknown).freeze
4
+
5
+ def self.get(type, service_name, arguments)
6
+ adapter =
7
+ if type == 'stdout'
8
+ if arguments.fetch(:json, false)
9
+ require_relative 'adapters/stdout_json_adapter'
10
+ StdoutJsonAdapter
11
+ else
12
+ require_relative 'adapters/stdout_adapter'
13
+ StdoutAdapter
14
+ end
15
+ else
16
+ raise "Unsupported logger: #{type}"
17
+ end
18
+ level = LOG_LEVELS.index(arguments.fetch(:level, 'debug'))
19
+ adapter.new(level, service_name, arguments)
20
+ end
21
+ end
22
+ end
@@ -0,0 +1,18 @@
1
+ class Lenjador
2
+ class NullLogger
3
+ def debug(*)
4
+ end
5
+
6
+ def info(*)
7
+ end
8
+
9
+ def warn(*)
10
+ end
11
+
12
+ def error(*)
13
+ end
14
+
15
+ def fatal(*)
16
+ end
17
+ end
18
+ end
@@ -0,0 +1,61 @@
1
+ class Lenjador
2
+ module Preprocessors
3
+ class Blacklist
4
+
5
+ DEFAULT_ACTION = 'prune'
6
+ MASK_SYMBOL = '*'
7
+ MASKED_VALUE = MASK_SYMBOL * 5
8
+
9
+ class UnsupportedActionException < Exception
10
+ end
11
+
12
+ def initialize(config = {})
13
+ @fields_to_process = config[:fields].inject({}) do |mem, field|
14
+ key = field.delete(:key)
15
+ options = {action: DEFAULT_ACTION}.merge(field)
16
+ validate_action_supported(options[:action])
17
+ mem.merge(key => options)
18
+ end
19
+ end
20
+
21
+ def process(data)
22
+ if data.is_a? Hash
23
+ data.inject({}) do |mem, (key, val)|
24
+ if (field = @fields_to_process[key.to_s])
25
+ self.send(action_method(field[:action]), mem, key, val)
26
+ else
27
+ mem.merge(key => process(val))
28
+ end
29
+ end
30
+ elsif data.is_a? Array
31
+ data.inject([]) do |mem, val|
32
+ mem + [process(val)]
33
+ end
34
+ else
35
+ data
36
+ end
37
+ end
38
+
39
+ private
40
+
41
+ def action_method(action)
42
+ "#{action}_field"
43
+ end
44
+
45
+ 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
+ end
49
+ end
50
+
51
+ def mask_field(data, key, val)
52
+ data.merge(key => MASKED_VALUE)
53
+ end
54
+
55
+ def prune_field(data, *)
56
+ data
57
+ end
58
+ alias_method :exclude_field, :prune_field
59
+ end
60
+ end
61
+ end
@@ -0,0 +1,40 @@
1
+ require 'lru_redux'
2
+
3
+ class Lenjador
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 Lenjador
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 Lenjador
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 'lenjador/preprocessors/json_pointer_trie'
2
+ require 'lenjador/preprocessors/strategies/mask'
3
+ require 'lenjador/preprocessors/strategies/prune'
4
+
5
+ class Lenjador
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,18 @@
1
+ class Lenjador
2
+ module Preprocessors
3
+ def self.get(type, arguments)
4
+ preprocessor =
5
+ case type.to_s
6
+ when 'blacklist'
7
+ require_relative 'preprocessors/blacklist'
8
+ Preprocessors::Blacklist
9
+ when 'whitelist'
10
+ require_relative 'preprocessors/whitelist'
11
+ Preprocessors::Whitelist
12
+ else
13
+ raise "Unknown preprocessor: #{type}"
14
+ end
15
+ preprocessor.new(arguments)
16
+ end
17
+ end
18
+ end