logged 0.0.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.
@@ -0,0 +1,109 @@
1
+ require 'logger'
2
+ require 'logged/tagged_logging'
3
+
4
+ module Logged
5
+ # Logger wrapping a component
6
+ class Logger
7
+ include TaggedLogging
8
+
9
+ attr_reader :loggers, :component
10
+
11
+ def initialize(loggers, component, formatter)
12
+ @loggers = loggers
13
+ @component = component
14
+ @formatter = formatter
15
+ end
16
+
17
+ def add(severity, message = nil, progname = nil)
18
+ message = yield if block_given? && message.blank?
19
+ message = progname if message.blank?
20
+
21
+ data, event = extract_data_and_event(message)
22
+
23
+ return if data.blank?
24
+
25
+ level = Logged.level_to_sym(severity)
26
+
27
+ @loggers.each do |logger, options|
28
+ next unless logger.send("#{level}?")
29
+
30
+ add_to_logger(level, event, data, logger, options)
31
+ end
32
+ end
33
+ alias_method :log, :add
34
+
35
+ %w(info debug warn error fatal unknown).each do |level|
36
+ class_eval <<-METHOD, __FILE__, __LINE__ + 1
37
+ def #{level}?
38
+ @loggers.keys.any? { |l| l.#{level}? }
39
+ end
40
+
41
+ def #{level}(progname = nil, &block)
42
+ add(::Logger::#{level.upcase}, nil, progname, &block)
43
+ end
44
+ METHOD
45
+ end
46
+
47
+ def close
48
+ @loggers.keys.each do |logger|
49
+ logger.close if logger.respond_to?(:close)
50
+ end
51
+ end
52
+
53
+ def datetime_format; end
54
+
55
+ def datetime_format=(_format); end
56
+
57
+ def <<(_msg); end
58
+
59
+ private
60
+
61
+ def prepare_data(event, data, options)
62
+ config = Logged.config[component].loggers[options[:name]]
63
+
64
+ return nil if Logged.ignore?(config, event)
65
+
66
+ Logged.custom_data(config, event, data)
67
+ end
68
+
69
+ def add_to_logger(level, event, data, logger, options)
70
+ data = prepare_data(event, data, options)
71
+
72
+ return if data.blank?
73
+
74
+ formatter = options[:formatter] || @formatter
75
+
76
+ msg = formatter.call(data)
77
+
78
+ return if msg.blank?
79
+
80
+ log_data(logger, level, msg)
81
+ end
82
+
83
+ def log_data(logger, level, msg)
84
+ if logger.respond_to?(:tagged)
85
+ logger.tagged(*current_tags) do
86
+ logger.send(level, msg)
87
+ end
88
+ else
89
+ logger.send(level, msg)
90
+ end
91
+ end
92
+
93
+ def extract_data_and_event(message)
94
+ return if message.blank?
95
+
96
+ message = { message: message } if message.is_a?(String)
97
+
98
+ event = message.delete('@event')
99
+
100
+ message = Logged.custom_data(Logged.config, event, message)
101
+ return [nil, nil] if message.blank?
102
+
103
+ message = Logged.custom_data(Logged.config[component], event, message)
104
+ return [nil, nil] if message.blank?
105
+
106
+ [message, event]
107
+ end
108
+ end
109
+ end
@@ -0,0 +1,73 @@
1
+ require 'action_dispatch/http/request'
2
+
3
+ module Logged
4
+ module Rack
5
+ # Handle tagged logging much like Rails::Rack::Logger
6
+ class Logger
7
+ def initialize(app)
8
+ @app = app
9
+ end
10
+
11
+ def call(env)
12
+ Thread.current[:logged_request_env] = env
13
+
14
+ request = ActionDispatch::Request.new(env)
15
+
16
+ loggers = Logged.components.map { |c| Logged.logger_by_component(c) }.compact.uniq
17
+
18
+ if loggers.length > 0
19
+ loggers_tagged(loggers, request) { @app.call(env) }
20
+ else
21
+ @app.call(env)
22
+ end
23
+ ensure
24
+ Thread.current[:logged_request_env] = nil
25
+ end
26
+
27
+ private
28
+
29
+ def loggers_tagged(loggers, request, &block)
30
+ logger = loggers.shift
31
+ tags = tags_for_component(logger.component, request)
32
+
33
+ if loggers.length > 0
34
+ tagged_block(logger, tags) { loggers_tagged(loggers, request, &block) }
35
+ else
36
+ tagged_block(logger, tags) { block.call }
37
+ end
38
+ end
39
+
40
+ def tagged_block(logger, tags, &block)
41
+ if logger.respond_to?(:tagged)
42
+ logger.tagged(*tags, &block)
43
+ else
44
+ dummy_tagged(&block)
45
+ end
46
+ end
47
+
48
+ def dummy_tagged
49
+ yield
50
+ end
51
+
52
+ def tags_for_component(component, request)
53
+ tags = Logged.config.tags || []
54
+ tags += Logged.config[component].tags || []
55
+
56
+ compute_tags(tags, request)
57
+ end
58
+
59
+ def compute_tags(tags, request)
60
+ tags.map do |tag|
61
+ case tag
62
+ when Proc
63
+ tag.call(request)
64
+ when Symbol
65
+ request.send(tag)
66
+ else
67
+ tag
68
+ end
69
+ end
70
+ end
71
+ end
72
+ end
73
+ end
@@ -0,0 +1,13 @@
1
+ require 'rails/railtie'
2
+ require 'logged/configuration'
3
+
4
+ module Logged
5
+ # Railtie for logged
6
+ class Railtie < Rails::Railtie
7
+ config.logged = Configuration.new
8
+
9
+ initializer :logged do |app|
10
+ Logged.setup(app) if app.config.logged.enabled
11
+ end
12
+ end
13
+ end
@@ -0,0 +1,30 @@
1
+ module Logged
2
+ # Tagged logging support
3
+ module TaggedLogging
4
+ def tagged(*tags)
5
+ new_tags = push_tags(*tags)
6
+
7
+ yield self
8
+ ensure
9
+ pop_tags(new_tags.size)
10
+ end
11
+
12
+ def flush
13
+ current_tags.clear
14
+ end
15
+
16
+ def push_tags(*tags)
17
+ tags.flatten.reject(&:blank?).tap do |new_tags|
18
+ current_tags.concat(new_tags)
19
+ end
20
+ end
21
+
22
+ def pop_tags(size = 1)
23
+ current_tags.pop(size)
24
+ end
25
+
26
+ def current_tags
27
+ Thread.current["logged_logger_tags_#{component}"] ||= []
28
+ end
29
+ end
30
+ end
@@ -0,0 +1,4 @@
1
+ module Logged
2
+ # Version
3
+ VERSION = '0.0.1'
4
+ end
data/lib/logged.rb ADDED
@@ -0,0 +1,220 @@
1
+ require 'logged/version'
2
+ require 'logged/level_conversion'
3
+ require 'logged/logger'
4
+ require 'logged/formatter/raw'
5
+ require 'logged/formatter/key_value'
6
+ require 'logged/formatter/json'
7
+ require 'logged/formatter/single_key'
8
+ require 'logged/formatter/logstash'
9
+ require 'logged/railtie'
10
+ require 'logged/rack/logger'
11
+
12
+ # logged
13
+ module Logged
14
+ extend Logged::LevelConversion
15
+
16
+ # special keys which not represent a component
17
+ CONFIG_KEYS = Configuration::DEFAULT_VALUES.keys + %i( loggers disable_rails_logging )
18
+
19
+ mattr_accessor :app, :config
20
+
21
+ # setup logged
22
+ def self.setup(app)
23
+ self.app = app
24
+ self.config = app.config.logged
25
+
26
+ app.config.middleware.insert_after ::Rails::Rack::Logger, Logged::Rack::Logger
27
+
28
+ components.each do |component|
29
+ remove_rails_subscriber(component) if config[component].disable_rails_logging
30
+
31
+ next unless config[component].enabled
32
+
33
+ enable_component(component)
34
+ end
35
+ end
36
+
37
+ # default log level
38
+ def self.default_level
39
+ config.level || :info
40
+ end
41
+
42
+ # default log formatter
43
+ def self.default_formatter
44
+ config.formatter || (@default_formatter ||= Logged::Formatter::KeyValue.new)
45
+ end
46
+
47
+ # logger wrapper for component
48
+ def self.logger_by_component(component)
49
+ return nil unless config.enabled
50
+
51
+ key = "component_#{component}"
52
+
53
+ return @component_loggers[key] if @component_loggers.key?(key)
54
+
55
+ loggers = loggers_for(component)
56
+
57
+ if loggers.blank?
58
+ @component_loggers[key] = nil
59
+
60
+ return nil
61
+ end
62
+
63
+ formatter = config[component].formatter || default_formatter
64
+
65
+ @component_loggers[key] = Logger.new(loggers, component, formatter)
66
+ end
67
+
68
+ # loggers for component
69
+ def self.loggers_for(component)
70
+ loggers_from_config(config)
71
+ .merge(loggers_from_config(config[component]))
72
+ end
73
+
74
+ def self.[](component)
75
+ loggers_for(component)
76
+ end
77
+
78
+ # loggers from config level
79
+ def self.loggers_from_config(conf)
80
+ loggers = {}
81
+
82
+ return loggers unless conf.enabled
83
+
84
+ conf.loggers.each do |name, c|
85
+ logger, options = load_logger(name, c)
86
+
87
+ next unless logger && options
88
+
89
+ loggers[logger] = options
90
+ end
91
+
92
+ loggers
93
+ end
94
+
95
+ # load logger from configuration
96
+ def self.load_logger(name, conf)
97
+ return [nil, nil] unless conf.enabled
98
+
99
+ options = conf.dup
100
+ options[:name] = name
101
+
102
+ logger = options.delete(:logger)
103
+
104
+ logger = Rails.logger if logger == :rails
105
+
106
+ return [nil, nil] unless logger
107
+
108
+ [logger, options]
109
+ end
110
+
111
+ # configure and enable component
112
+ def self.enable_component(component)
113
+ loggers = loggers_for(component)
114
+
115
+ loggers.each do |logger, options|
116
+ level = options[:level] || config[component].level || default_level
117
+
118
+ logger.level = level_to_const(level) if logger.respond_to?(:'level=')
119
+ end
120
+
121
+ # only attach subscribers with loggers
122
+ if loggers.any?
123
+ @subscribers[component].each do |subscriber|
124
+ subscriber.attach_to(component)
125
+ end
126
+ end
127
+ end
128
+
129
+ # check if event should be ignored
130
+ def self.ignore?(conf, event)
131
+ return false unless event
132
+ return false unless conf.enabled
133
+
134
+ if !event.is_a?(String) && conf.ignore.is_a?(Array)
135
+ return true if conf.ignore.include?(event.name)
136
+ end
137
+
138
+ if conf.custom_ignore.respond_to?(:call)
139
+ return conf.custom_ignore.call(event)
140
+ end
141
+
142
+ false
143
+ end
144
+
145
+ # run data callbacks
146
+ def self.custom_data(conf, event, data)
147
+ return data unless conf.enabled
148
+ return data unless conf.custom_data.respond_to?(:call)
149
+
150
+ conf.custom_data.call(event, data)
151
+ end
152
+
153
+ # configured components
154
+ def self.components
155
+ config.keys - CONFIG_KEYS
156
+ end
157
+
158
+ # remove rails log subscriber by component name
159
+ def self.remove_rails_subscriber(component)
160
+ subscriber = rails_subscriber(component)
161
+
162
+ return unless subscriber
163
+
164
+ unsubscribe(component, subscriber)
165
+ end
166
+
167
+ # try to guess and get rails log subscriber by component name
168
+ def self.rails_subscriber(component)
169
+ class_name = "::#{component.to_s.camelize}::LogSubscriber"
170
+
171
+ return unless Object.const_defined?(class_name)
172
+
173
+ clazz = class_name.constantize
174
+
175
+ ActiveSupport::LogSubscriber.log_subscribers.each do |subscriber|
176
+ return subscriber if subscriber.is_a?(clazz)
177
+ end
178
+
179
+ nil
180
+ end
181
+
182
+ # unsubscribe a subscriber from a component
183
+ def self.unsubscribe(component, subscriber)
184
+ events = subscriber.public_methods(false).reject { |method| method.to_s == 'call' }
185
+
186
+ events.each do |event|
187
+ ActiveSupport::Notifications.notifier.listeners_for("#{event}.#{component}").each do |listener|
188
+ if listener.instance_variable_get('@delegate') == subscriber
189
+ ActiveSupport::Notifications.unsubscribe listener
190
+ end
191
+ end
192
+ end
193
+ end
194
+
195
+ # register log subscriber with logged
196
+ def self.register(component, subscriber)
197
+ return if @subscribers[component].include?(subscriber)
198
+
199
+ @subscribers[component] << subscriber
200
+ end
201
+
202
+ def self.request_env
203
+ Thread.current[:logged_request_env]
204
+ end
205
+
206
+ private
207
+
208
+ def self.init
209
+ @subscribers ||= Hash.new { |hash, key| hash[key] = [] }
210
+
211
+ @component_loggers = {}
212
+ end
213
+
214
+ init
215
+ end
216
+
217
+ require 'logged/log_subscriber/action_controller' if defined?(ActionController)
218
+ require 'logged/log_subscriber/action_view' if defined?(ActionView)
219
+ require 'logged/log_subscriber/active_record' if defined?(ActiveRecord)
220
+ require 'logged/log_subscriber/action_mailer' if defined?(ActionMailer)
data/logged.gemspec ADDED
@@ -0,0 +1,31 @@
1
+ # coding: utf-8
2
+ lib = File.expand_path('../lib', __FILE__)
3
+ $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
4
+ require 'logged/version'
5
+
6
+ Gem::Specification.new do |spec|
7
+ rails_version = '>= 4.0', '< 5.0'
8
+
9
+ spec.name = 'logged'
10
+ spec.version = Logged::VERSION
11
+ spec.authors = ['Florian Schwab']
12
+ spec.email = ['me@ydkn.de']
13
+ spec.summary = %q(Better logging for rails)
14
+ spec.homepage = ''
15
+ spec.license = 'MIT'
16
+
17
+ spec.files = `git ls-files -z`.split("\x0")
18
+ spec.executables = spec.files.grep(%r{^bin/}) { |f| File.basename(f) }
19
+ spec.test_files = spec.files.grep(%r{^(test|spec|features)/})
20
+ spec.require_paths = ['lib']
21
+
22
+ spec.add_development_dependency 'bundler', '~> 1.7'
23
+ spec.add_development_dependency 'rake', '~> 10.0'
24
+ spec.add_development_dependency 'rspec', '~> 3.1'
25
+ spec.add_development_dependency 'actionpack', rails_version
26
+ spec.add_development_dependency 'actionview', rails_version
27
+ spec.add_development_dependency 'actionmailer', rails_version
28
+ spec.add_development_dependency 'activerecord', rails_version
29
+
30
+ spec.add_dependency 'railties', rails_version
31
+ end
@@ -0,0 +1,10 @@
1
+ require 'spec_helper'
2
+ require 'logged'
3
+
4
+ RSpec.describe Logged::Formatter::JSON do
5
+ subject { described_class.new }
6
+
7
+ it 'serializes data' do
8
+ expect(subject.call(foo: 'bar')).to eq('{"foo":"bar"}')
9
+ end
10
+ end
@@ -0,0 +1,10 @@
1
+ require 'spec_helper'
2
+ require 'logged'
3
+
4
+ RSpec.describe Logged::Formatter::KeyValue do
5
+ subject { described_class.new }
6
+
7
+ it 'serializes data' do
8
+ expect(subject.call(foo: 'bar')).to eq("foo='bar'")
9
+ end
10
+ end
@@ -0,0 +1,10 @@
1
+ require 'spec_helper'
2
+ require 'logged'
3
+
4
+ RSpec.describe Logged::Formatter::Raw do
5
+ subject { described_class.new }
6
+
7
+ it 'serializes data' do
8
+ expect(subject.call(foo: 'bar')).to eq(foo: 'bar')
9
+ end
10
+ end
@@ -0,0 +1,63 @@
1
+ require 'spec_helper'
2
+ require 'logged'
3
+ require 'active_support/notifications'
4
+ require 'active_support/log_subscriber'
5
+ require 'action_controller/log_subscriber'
6
+ require 'action_view/log_subscriber'
7
+ require 'action_mailer/log_subscriber'
8
+ require 'active_record/log_subscriber'
9
+
10
+ RSpec.describe Logged do
11
+ context 'when removing Rails log subscribers' do
12
+ after do
13
+ log_subscribers = []
14
+
15
+ ActiveSupport::LogSubscriber.log_subscribers.each do |subscriber|
16
+ events = subscriber.public_methods(false).reject { |method| method.to_s == 'call' }
17
+ events.each do |event|
18
+ ActiveSupport::Notifications.notifier.listeners_for("#{event}.#{subscriber.class.to_s.split('::').first.underscore}").each do |listener|
19
+ delegate = listener.instance_variable_get('@delegate')
20
+ log_subscribers << subscriber.class if delegate == subscriber
21
+ end
22
+ end
23
+ end
24
+
25
+ ActionController::LogSubscriber.attach_to :action_controller unless log_subscribers.include?(ActionController::LogSubscriber)
26
+ ActionView::LogSubscriber.attach_to :action_view unless log_subscribers.include?(ActionView::LogSubscriber)
27
+ ActionMailer::LogSubscriber.attach_to :action_mailer unless log_subscribers.include?(ActionMailer::LogSubscriber)
28
+ ActiveRecord::LogSubscriber.attach_to :active_record unless log_subscribers.include?(ActiveRecord::LogSubscriber)
29
+ end
30
+
31
+ it 'removes subscribers for action_controller events' do
32
+ expect {
33
+ Logged.remove_rails_subscriber(:action_controller)
34
+ }.to change {
35
+ ActiveSupport::Notifications.notifier.listeners_for('process_action.action_controller')
36
+ }
37
+ end
38
+
39
+ it 'removes subscribers for action_view events' do
40
+ expect {
41
+ Logged.remove_rails_subscriber(:action_view)
42
+ }.to change {
43
+ ActiveSupport::Notifications.notifier.listeners_for('render_template.action_view')
44
+ }
45
+ end
46
+
47
+ it 'removes subscribers for action_mailer events' do
48
+ expect {
49
+ Logged.remove_rails_subscriber(:action_mailer)
50
+ }.to change {
51
+ ActiveSupport::Notifications.notifier.listeners_for('deliver.action_mailer')
52
+ }
53
+ end
54
+
55
+ it 'removes subscribers for active_record events' do
56
+ expect {
57
+ Logged.remove_rails_subscriber(:active_record)
58
+ }.to change {
59
+ ActiveSupport::Notifications.notifier.listeners_for('sql.active_record')
60
+ }
61
+ end
62
+ end
63
+ end
@@ -0,0 +1,87 @@
1
+ # This file was generated by the `rspec --init` command. Conventionally, all
2
+ # specs live under a `spec` directory, which RSpec adds to the `$LOAD_PATH`.
3
+ # The generated `.rspec` file contains `--require spec_helper` which will cause this
4
+ # file to always be loaded, without a need to explicitly require it in any files.
5
+ #
6
+ # Given that it is always loaded, you are encouraged to keep this file as
7
+ # light-weight as possible. Requiring heavyweight dependencies from this file
8
+ # will add to the boot time of your test suite on EVERY test run, even for an
9
+ # individual file that may not need all of that loaded. Instead, consider making
10
+ # a separate helper file that requires the additional dependencies and performs
11
+ # the additional setup, and require it from the spec files that actually need it.
12
+ #
13
+ # The `.rspec` file also contains a few flags that are not defaults but that
14
+ # users commonly want.
15
+ #
16
+ # See http://rubydoc.info/gems/rspec-core/RSpec/Core/Configuration
17
+ RSpec.configure do |config|
18
+ # rspec-expectations config goes here. You can use an alternate
19
+ # assertion/expectation library such as wrong or the stdlib/minitest
20
+ # assertions if you prefer.
21
+ config.expect_with :rspec do |expectations|
22
+ # This option will default to `true` in RSpec 4. It makes the `description`
23
+ # and `failure_message` of custom matchers include text for helper methods
24
+ # defined using `chain`, e.g.:
25
+ # be_bigger_than(2).and_smaller_than(4).description
26
+ # # => "be bigger than 2 and smaller than 4"
27
+ # ...rather than:
28
+ # # => "be bigger than 2"
29
+ expectations.include_chain_clauses_in_custom_matcher_descriptions = true
30
+ end
31
+
32
+ # rspec-mocks config goes here. You can use an alternate test double
33
+ # library (such as bogus or mocha) by changing the `mock_with` option here.
34
+ config.mock_with :rspec do |mocks|
35
+ # Prevents you from mocking or stubbing a method that does not exist on
36
+ # a real object. This is generally recommended, and will default to
37
+ # `true` in RSpec 4.
38
+ mocks.verify_partial_doubles = true
39
+ end
40
+
41
+ # The settings below are suggested to provide a good initial experience
42
+ # with RSpec, but feel free to customize to your heart's content.
43
+ # These two settings work together to allow you to limit a spec run
44
+ # to individual examples or groups you care about by tagging them with
45
+ # `:focus` metadata. When nothing is tagged with `:focus`, all examples
46
+ # get run.
47
+ config.filter_run :focus
48
+ config.run_all_when_everything_filtered = true
49
+
50
+ # Limits the available syntax to the non-monkey patched syntax that is recommended.
51
+ # For more details, see:
52
+ # - http://myronmars.to/n/dev-blog/2012/06/rspecs-new-expectation-syntax
53
+ # - http://teaisaweso.me/blog/2013/05/27/rspecs-new-message-expectation-syntax/
54
+ # - http://myronmars.to/n/dev-blog/2014/05/notable-changes-in-rspec-3#new__config_option_to_disable_rspeccore_monkey_patching
55
+ config.disable_monkey_patching!
56
+
57
+ # This setting enables warnings. It's recommended, but in some cases may
58
+ # be too noisy due to issues in dependencies.
59
+ config.warnings = true
60
+
61
+ # Many RSpec users commonly either run the entire suite or an individual
62
+ # file, and it's useful to allow more verbose output when running an
63
+ # individual spec file.
64
+ if config.files_to_run.one?
65
+ # Use the documentation formatter for detailed output,
66
+ # unless a formatter has already been configured
67
+ # (e.g. via a command-line flag).
68
+ config.default_formatter = 'doc'
69
+ end
70
+
71
+ # Print the 10 slowest examples and example groups at the
72
+ # end of the spec run, to help surface which specs are running
73
+ # particularly slow.
74
+ config.profile_examples = 10
75
+
76
+ # Run specs in random order to surface order dependencies. If you find an
77
+ # order dependency and want to debug it, you can fix the order by providing
78
+ # the seed, which is printed after each run.
79
+ # --seed 1234
80
+ config.order = :random
81
+
82
+ # Seed global randomization in this process using the `--seed` CLI option.
83
+ # Setting this allows you to use `--seed` to deterministically reproduce
84
+ # test failures related to randomization by passing the same `--seed` value
85
+ # as the one that triggered the failure.
86
+ Kernel.srand config.seed
87
+ end