lenjador 1.2.1

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