epilog 0.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.
data/bin/rspec ADDED
@@ -0,0 +1,18 @@
1
+ #!/usr/bin/env ruby
2
+ # frozen_string_literal: true
3
+
4
+ #
5
+ # This file was generated by Bundler.
6
+ #
7
+ # The application 'rspec' is installed as part of a gem, and
8
+ # this file is here to facilitate running it.
9
+ #
10
+
11
+ require 'pathname'
12
+ ENV['BUNDLE_GEMFILE'] ||= File.expand_path('../../Gemfile',
13
+ Pathname.new(__FILE__).realpath)
14
+
15
+ require 'rubygems'
16
+ require 'bundler/setup'
17
+
18
+ load Gem.bin_path('rspec-core', 'rspec')
data/bin/rubocop ADDED
@@ -0,0 +1,18 @@
1
+ #!/usr/bin/env ruby
2
+ # frozen_string_literal: true
3
+
4
+ #
5
+ # This file was generated by Bundler.
6
+ #
7
+ # The application 'rubocop' is installed as part of a gem, and
8
+ # this file is here to facilitate running it.
9
+ #
10
+
11
+ require 'pathname'
12
+ ENV['BUNDLE_GEMFILE'] ||= File.expand_path('../../Gemfile',
13
+ Pathname.new(__FILE__).realpath)
14
+
15
+ require 'rubygems'
16
+ require 'bundler/setup'
17
+
18
+ load Gem.bin_path('rubocop', 'rubocop')
data/bin/yard ADDED
@@ -0,0 +1,18 @@
1
+ #!/usr/bin/env ruby
2
+ # frozen_string_literal: true
3
+
4
+ #
5
+ # This file was generated by Bundler.
6
+ #
7
+ # The application 'yard' is installed as part of a gem, and
8
+ # this file is here to facilitate running it.
9
+ #
10
+
11
+ require 'pathname'
12
+ ENV['BUNDLE_GEMFILE'] ||= File.expand_path('../../Gemfile',
13
+ Pathname.new(__FILE__).realpath)
14
+
15
+ require 'rubygems'
16
+ require 'bundler/setup'
17
+
18
+ load Gem.bin_path('yard', 'yard')
data/bin/yardoc ADDED
@@ -0,0 +1,18 @@
1
+ #!/usr/bin/env ruby
2
+ # frozen_string_literal: true
3
+
4
+ #
5
+ # This file was generated by Bundler.
6
+ #
7
+ # The application 'yardoc' is installed as part of a gem, and
8
+ # this file is here to facilitate running it.
9
+ #
10
+
11
+ require 'pathname'
12
+ ENV['BUNDLE_GEMFILE'] ||= File.expand_path('../../Gemfile',
13
+ Pathname.new(__FILE__).realpath)
14
+
15
+ require 'rubygems'
16
+ require 'bundler/setup'
17
+
18
+ load Gem.bin_path('yard', 'yardoc')
data/bin/yri ADDED
@@ -0,0 +1,18 @@
1
+ #!/usr/bin/env ruby
2
+ # frozen_string_literal: true
3
+
4
+ #
5
+ # This file was generated by Bundler.
6
+ #
7
+ # The application 'yri' is installed as part of a gem, and
8
+ # this file is here to facilitate running it.
9
+ #
10
+
11
+ require 'pathname'
12
+ ENV['BUNDLE_GEMFILE'] ||= File.expand_path('../../Gemfile',
13
+ Pathname.new(__FILE__).realpath)
14
+
15
+ require 'rubygems'
16
+ require 'bundler/setup'
17
+
18
+ load Gem.bin_path('yard', 'yri')
data/epilog.gemspec ADDED
@@ -0,0 +1,34 @@
1
+ # frozen_string_literal: true
2
+
3
+ lib = File.expand_path('lib', __dir__)
4
+ $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
5
+ require 'epilog/version'
6
+
7
+ Gem::Specification.new do |spec|
8
+ spec.name = 'epilog'
9
+ spec.version = Epilog::VERSION
10
+ spec.authors = ['Justin Howard']
11
+ spec.email = ['jmhoward0@gmail.com']
12
+ spec.license = 'Apache-2.0'
13
+
14
+ spec.summary = 'A JSON logger with Rails support'
15
+ spec.homepage = 'https://github.com/machinima/epilog'
16
+
17
+ spec.files = `git ls-files -z`
18
+ .split("\x0")
19
+ .reject { |f| f.match(%r{^spec/}) }
20
+ spec.require_paths = ['lib']
21
+
22
+ spec.add_development_dependency 'bundler', '~> 1.12'
23
+ spec.add_development_dependency 'byebug', '~> 9.0'
24
+ spec.add_development_dependency 'combustion', '~> 1.0.0'
25
+ spec.add_development_dependency 'rails', '>= 4.2', '< 6'
26
+ spec.add_development_dependency 'rake', '~> 10.0'
27
+ spec.add_development_dependency 'redcarpet', '~> 3.4'
28
+ spec.add_development_dependency 'rspec', '~> 3.4'
29
+ spec.add_development_dependency 'rspec-rails', '~> 3.8.1'
30
+ spec.add_development_dependency 'rubocop', '~> 0.61'
31
+ spec.add_development_dependency 'simplecov', '~> 0.12'
32
+ spec.add_development_dependency 'sqlite3', '~> 1.3'
33
+ spec.add_development_dependency 'yard', '~> 0.9.11'
34
+ end
data/lib/epilog.rb ADDED
@@ -0,0 +1,10 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'logger'
4
+
5
+ require 'epilog/version'
6
+ require 'epilog/logger'
7
+ require 'epilog/mock_logger'
8
+ require 'epilog/log_formatter'
9
+ require 'epilog/filter'
10
+ require 'epilog/rails' if defined?(Rails)
@@ -0,0 +1,4 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'epilog/filter/hash_key'
4
+ require 'epilog/filter/blacklist'
@@ -0,0 +1,30 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Epilog
4
+ module Filter
5
+ class Blacklist < HashKey
6
+ DEFAULT_BLACKLIST = %w[
7
+ password
8
+ pass
9
+ pw
10
+ secret
11
+ ].freeze
12
+
13
+ attr_reader :blacklist
14
+
15
+ def initialize(blacklist = DEFAULT_BLACKLIST)
16
+ @blacklist = Hash[blacklist.map { |b| [b.to_s.downcase, nil] }]
17
+ end
18
+
19
+ private
20
+
21
+ def key?(key)
22
+ @blacklist.key?(key.to_s.downcase)
23
+ end
24
+
25
+ def filter(value)
26
+ "[filtered #{value.class.name}]"
27
+ end
28
+ end
29
+ end
30
+ end
@@ -0,0 +1,41 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Epilog
4
+ module Filter
5
+ class HashKey
6
+ def call(log)
7
+ fix(log)
8
+ end
9
+
10
+ private
11
+
12
+ def fix(value)
13
+ if value.is_a?(Hash)
14
+ fix_hash(value)
15
+ elsif value.is_a?(Array)
16
+ value.map { |i| fix(i) }
17
+ else
18
+ value
19
+ end
20
+ end
21
+
22
+ def fix_hash(hash)
23
+ hash.each_with_object({}) do |(key, value), obj|
24
+ obj[key] = if key?(key)
25
+ filter(value)
26
+ else
27
+ fix(value)
28
+ end
29
+ end
30
+ end
31
+
32
+ def key?(_key)
33
+ true
34
+ end
35
+
36
+ def filter(value)
37
+ "[filtered #{value.class.name}]"
38
+ end
39
+ end
40
+ end
41
+ end
@@ -0,0 +1,67 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Epilog
4
+ class Formatter
5
+ SEVERITY_MAP = {
6
+ 'FATAL' => 'ALERT',
7
+ 'WARN' => 'WARNING'
8
+ }.freeze
9
+
10
+ DEFAULT_TIME_FORMAT = '%Y-%m-%dT%H:%M:%S%z'
11
+
12
+ attr_reader :filter
13
+ attr_writer :datetime_format
14
+
15
+ def initialize(options = {})
16
+ @filter = options[:filter] || Filter::Blacklist.new
17
+ end
18
+
19
+ def call(severity, time, progname, msg)
20
+ log = base_log(severity, time, progname)
21
+ log.merge!(message(msg))
22
+
23
+ if log[:exception].is_a?(Exception)
24
+ log[:exception] = format_error(log[:exception])
25
+ end
26
+
27
+ log = before_write(log)
28
+ "#{JSON.dump(log)}\n"
29
+ end
30
+
31
+ def datetime_format
32
+ @datetime_format || DEFAULT_TIME_FORMAT
33
+ end
34
+
35
+ private
36
+
37
+ def base_log(severity, time, progname)
38
+ {
39
+ timestamp: time.strftime(datetime_format),
40
+ severity: SEVERITY_MAP[severity] || severity,
41
+ source: progname
42
+ }
43
+ end
44
+
45
+ def message(msg)
46
+ return { message: msg.message, exception: msg } if msg.is_a?(Exception)
47
+ return msg.to_h if msg.respond_to?(:to_h)
48
+
49
+ { message: msg.to_s }
50
+ end
51
+
52
+ def format_error(error)
53
+ hash = {
54
+ name: error.class.name,
55
+ message: error.message,
56
+ trace: error.backtrace
57
+ }
58
+ cause = error.cause
59
+ hash[:parent] = format_error(cause) unless cause.nil?
60
+ hash
61
+ end
62
+
63
+ def before_write(log)
64
+ @filter ? @filter.call(log) : log
65
+ end
66
+ end
67
+ end
@@ -0,0 +1,22 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Epilog
4
+ class Logger < ::Logger
5
+ def initialize(*args, **options)
6
+ super
7
+ self.formatter = Formatter.new
8
+ end
9
+
10
+ def datetime_format
11
+ return unless formatter
12
+
13
+ formatter.datetime_format
14
+ end
15
+
16
+ def datetime_format=(format)
17
+ return unless formatter
18
+
19
+ formatter.datetime_format = format
20
+ end
21
+ end
22
+ end
@@ -0,0 +1,59 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Epilog
4
+ class MockLogger < ::Logger
5
+ def initialize
6
+ super(nil)
7
+ reset
8
+ end
9
+
10
+ # rubocop:disable MethodLength
11
+ def add(severity, message = nil, progname = nil)
12
+ severity ||= Logger::UNKNOWN
13
+ return true if severity < level
14
+
15
+ prog ||= progname
16
+ if message.nil?
17
+ if block_given?
18
+ message = yield
19
+ else
20
+ message = prog
21
+ prog = progname
22
+ end
23
+ end
24
+
25
+ write(format_severity(severity), current_time, prog, message)
26
+ end
27
+ # rubocop:enable MethodLength
28
+ alias log add
29
+
30
+ def reopen(_logdev = nil)
31
+ end
32
+
33
+ def [](index)
34
+ @logs[index].dup || []
35
+ end
36
+
37
+ def to_a
38
+ (0...@logs.size).map { |i| self[i] }
39
+ end
40
+
41
+ def freeze_time(time)
42
+ @time = time
43
+ end
44
+
45
+ def reset
46
+ @logs = []
47
+ end
48
+
49
+ private
50
+
51
+ def current_time
52
+ @time || Time.now
53
+ end
54
+
55
+ def write(severity, time, prog, message)
56
+ @logs << [severity, time, prog, message]
57
+ end
58
+ end
59
+ end
@@ -0,0 +1,23 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'action_controller/log_subscriber'
4
+ require 'action_dispatch/middleware/debug_exceptions'
5
+ require 'action_mailer/log_subscriber'
6
+ require 'action_view/log_subscriber'
7
+ require 'active_record/log_subscriber'
8
+ require 'active_job/logging'
9
+
10
+ require 'epilog/rails/ext/event_delegate'
11
+ require 'epilog/rails/ext/active_support_logger'
12
+ require 'epilog/rails/ext/rack_logger'
13
+ require 'epilog/rails/ext/action_controller'
14
+ require 'epilog/rails/ext/debug_exceptions'
15
+
16
+ require 'epilog/rails/epilog_ext'
17
+ require 'epilog/rails/log_subscriber'
18
+ require 'epilog/rails/action_controller_subscriber'
19
+ require 'epilog/rails/action_mailer_subscriber'
20
+ require 'epilog/rails/action_view_subscriber'
21
+ require 'epilog/rails/active_record_subscriber'
22
+ require 'epilog/rails/active_job_subscriber'
23
+ require 'epilog/rails/railtie'
@@ -0,0 +1,153 @@
1
+ # frozen_string_literal: true
2
+
3
+ # rubocop:disable ClassLength
4
+ module Epilog
5
+ module Rails
6
+ class ActionControllerSubscriber < LogSubscriber
7
+ RAILS_PARAMS = %i[controller action format _method only_path].freeze
8
+
9
+ def request_received(event)
10
+ info do
11
+ {
12
+ message: "#{request_string(event)} started",
13
+ request: request_hash(event)
14
+ }
15
+ end
16
+ end
17
+
18
+ def process_request(event)
19
+ info do
20
+ {
21
+ message: response_string(event),
22
+ request: short_request_hash(event),
23
+ response: response_hash(event),
24
+ metrics: process_metrics(event.payload[:metrics]
25
+ .merge(request_runtime: event.duration.round(2)))
26
+ }
27
+ end
28
+ end
29
+
30
+ def start_processing(*)
31
+ end
32
+
33
+ def process_action(*)
34
+ end
35
+
36
+ def send_data(event)
37
+ info { basic_message(event, "Sent data #{event.payload[:filename]}") }
38
+ end
39
+
40
+ def send_file(event)
41
+ info { basic_message(event, "Sent file #{event.payload[:path]}") }
42
+ end
43
+
44
+ def redirect_to(event)
45
+ info { basic_message(event, "Redirect > #{event.payload[:location]}") }
46
+ end
47
+
48
+ def halted_callback(event)
49
+ info do
50
+ basic_message(event, 'Filter chain halted as ' \
51
+ "#{event.payload[:filter].inspect} rendered or redirected")
52
+ end
53
+ end
54
+
55
+ def unpermitted_parameters(event)
56
+ debug do
57
+ basic_message(event, 'Unpermitted parameters: ' \
58
+ "#{event.payload[:keys].join(', ')}")
59
+ end
60
+ end
61
+
62
+ %i[
63
+ write_fragment
64
+ read_fragment
65
+ exist_fragment?
66
+ expire_fragment
67
+ expire_page
68
+ write_page
69
+ ].each do |method|
70
+ define_method(method) do |event|
71
+ return unless logger.info?
72
+
73
+ path = Array(event.payload[:key] || event.payload[:path]).join('/')
74
+ debug(basic_message(event, "#{method} #{path}"))
75
+ end
76
+ end
77
+
78
+ private
79
+
80
+ def request_hash(event) # rubocop:disable AbcSize, MethodLength
81
+ request = event.payload[:request]
82
+ {
83
+ id: request.uuid,
84
+ ip: request.remote_ip,
85
+ host: request.host,
86
+ protocol: request.protocol.to_s.gsub('://', ''),
87
+ method: request.request_method,
88
+ port: request.port,
89
+ path: request.fullpath,
90
+ query: request.query_parameters,
91
+ cookies: request.cookies,
92
+ headers: request.headers.to_h.keep_if do |key, _value|
93
+ key =~ ActionDispatch::Http::Headers::HTTP_HEADER
94
+ end,
95
+ params: request.filtered_parameters.except(*RAILS_PARAMS),
96
+ format: request.format.try(:ref),
97
+ controller: event.payload[:controller],
98
+ action: event.payload[:action]
99
+ }
100
+ end
101
+
102
+ def short_request_hash(event)
103
+ request = event.payload[:request]
104
+ {
105
+ id: request.uuid,
106
+ method: request.method,
107
+ path: request.fullpath
108
+ }
109
+ end
110
+
111
+ def request_string(event)
112
+ request = event.payload[:request]
113
+ "#{request.request_method} #{request.fullpath}"
114
+ end
115
+
116
+ def response_hash(event)
117
+ { status: normalize_status(event) }
118
+ end
119
+
120
+ def response_string(event)
121
+ status = normalize_status(event)
122
+ status_string = Rack::Utils::HTTP_STATUS_CODES[status]
123
+ "#{request_string(event)} > #{status} #{status_string}"
124
+ end
125
+
126
+ def normalize_status(event)
127
+ payload = event.payload
128
+ status = payload[:response].status
129
+ if status.nil? && payload[:exception].present?
130
+ status = ActionDispatch::ExceptionWrapper
131
+ .status_code_for_exception(payload[:exception].first)
132
+ end
133
+ status
134
+ end
135
+
136
+ def basic_message(event, message)
137
+ {
138
+ message: message,
139
+ metrics: process_metrics(duration: event.duration)
140
+ }
141
+ end
142
+
143
+ def process_metrics(metrics)
144
+ metrics.each_with_object({}) do |(key, value), obj|
145
+ next if value.nil?
146
+
147
+ obj[key] = value.round(2) if value.is_a?(Numeric)
148
+ end
149
+ end
150
+ end
151
+ end
152
+ end
153
+ # rubocop:enable ClassLength