logged 0.0.1

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