rails-logstasher 0.1.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.
Files changed (76) hide show
  1. checksums.yaml +15 -0
  2. data/MIT-LICENSE +20 -0
  3. data/README.md +100 -0
  4. data/Rakefile +38 -0
  5. data/lib/rails_logstasher.rb +16 -0
  6. data/lib/rails_logstasher/action_controller/log_subscriber.rb +75 -0
  7. data/lib/rails_logstasher/action_view/log_subscriber.rb +31 -0
  8. data/lib/rails_logstasher/active_record/log_subscriber.rb +88 -0
  9. data/lib/rails_logstasher/active_resource/log_subscriber.rb +34 -0
  10. data/lib/rails_logstasher/core_ext/object/blank.rb +110 -0
  11. data/lib/rails_logstasher/event.rb +44 -0
  12. data/lib/rails_logstasher/logger.rb +55 -0
  13. data/lib/rails_logstasher/rack/logger.rb +53 -0
  14. data/lib/rails_logstasher/railtie.rb +70 -0
  15. data/lib/rails_logstasher/tagged_logging.rb +108 -0
  16. data/lib/rails_logstasher/version.rb +3 -0
  17. data/test/action_controller/log_subscriber_test.rb +165 -0
  18. data/test/action_view/log_subscriber_test.rb +89 -0
  19. data/test/active_record/log_subscriber_test.rb +87 -0
  20. data/test/active_resource/log_subscriber_test.rb +42 -0
  21. data/test/core_ext/blank_test.rb +31 -0
  22. data/test/dummy/README.rdoc +261 -0
  23. data/test/dummy/Rakefile +7 -0
  24. data/test/dummy/app/assets/javascripts/application.js +13 -0
  25. data/test/dummy/app/assets/javascripts/widgets.js +2 -0
  26. data/test/dummy/app/assets/stylesheets/application.css +13 -0
  27. data/test/dummy/app/assets/stylesheets/scaffold.css +56 -0
  28. data/test/dummy/app/assets/stylesheets/widgets.css +4 -0
  29. data/test/dummy/app/controllers/application_controller.rb +3 -0
  30. data/test/dummy/app/controllers/log_subscriber_controller.rb +56 -0
  31. data/test/dummy/app/controllers/widgets_controller.rb +83 -0
  32. data/test/dummy/app/helpers/application_helper.rb +2 -0
  33. data/test/dummy/app/helpers/widgets_helper.rb +2 -0
  34. data/test/dummy/app/models/widget.rb +3 -0
  35. data/test/dummy/app/views/customers/_customer.html.erb +1 -0
  36. data/test/dummy/app/views/good_customers/_good_customer.html.erb +1 -0
  37. data/test/dummy/app/views/layouts/application.html.erb +14 -0
  38. data/test/dummy/app/views/test/_customer.erb +1 -0
  39. data/test/dummy/app/views/test/hello_world.erb +1 -0
  40. data/test/dummy/app/views/widgets/_form.html.erb +17 -0
  41. data/test/dummy/app/views/widgets/edit.html.erb +6 -0
  42. data/test/dummy/app/views/widgets/index.html.erb +21 -0
  43. data/test/dummy/app/views/widgets/new.html.erb +5 -0
  44. data/test/dummy/app/views/widgets/show.html.erb +5 -0
  45. data/test/dummy/config.ru +4 -0
  46. data/test/dummy/config/application.rb +69 -0
  47. data/test/dummy/config/boot.rb +10 -0
  48. data/test/dummy/config/database.yml +22 -0
  49. data/test/dummy/config/environment.rb +5 -0
  50. data/test/dummy/config/environments/development.rb +31 -0
  51. data/test/dummy/config/environments/production.rb +64 -0
  52. data/test/dummy/config/environments/test.rb +35 -0
  53. data/test/dummy/config/initializers/backtrace_silencers.rb +7 -0
  54. data/test/dummy/config/initializers/inflections.rb +15 -0
  55. data/test/dummy/config/initializers/mime_types.rb +5 -0
  56. data/test/dummy/config/initializers/secret_token.rb +7 -0
  57. data/test/dummy/config/initializers/session_store.rb +8 -0
  58. data/test/dummy/config/initializers/wrap_parameters.rb +10 -0
  59. data/test/dummy/config/locales/en.yml +5 -0
  60. data/test/dummy/config/routes.rb +69 -0
  61. data/test/dummy/db/migrate/20120927084605_create_widgets.rb +8 -0
  62. data/test/dummy/db/schema.rb +16 -0
  63. data/test/dummy/public/404.html +26 -0
  64. data/test/dummy/public/422.html +26 -0
  65. data/test/dummy/public/500.html +25 -0
  66. data/test/dummy/public/favicon.ico +0 -0
  67. data/test/dummy/script/rails +6 -0
  68. data/test/logger_test.rb +126 -0
  69. data/test/rack/logger_test.rb +68 -0
  70. data/test/rails_logstasher_test.rb +7 -0
  71. data/test/support/fake_models.rb +12 -0
  72. data/test/support/integration_case.rb +5 -0
  73. data/test/support/multibyte_test_helpers.rb +19 -0
  74. data/test/tagged_logging_test.rb +155 -0
  75. data/test/test_helper.rb +40 -0
  76. metadata +219 -0
@@ -0,0 +1,44 @@
1
+ module RailsLogstasher
2
+
3
+ # Basically a wrapper for a LogStash event that keeps track of if it was created from a rack
4
+ # middle-ware or not. This is important when it comes to deciding when to write the log
5
+ class Event
6
+
7
+ extend Forwardable
8
+ def_delegators :@logstash_event, :fields, :message=, :source=, :type=, :tags, :to_json
9
+
10
+ def initialize(logger, rack = false)
11
+ @logger = logger
12
+ @rack = rack
13
+ @logstash_event = LogStash::Event.new
14
+ end
15
+
16
+ def write(rack = false)
17
+ if @rack
18
+ @logger.info self if rack
19
+ else
20
+ @logger.info self
21
+ end
22
+ end
23
+
24
+ def add_tags_to_logger(request, tags)
25
+ tag_hash = []
26
+ if tags
27
+ tags.each do |tag|
28
+ case tag
29
+ when Symbol
30
+ tag_hash << {tag.to_s => request.send(tag) }
31
+ when Proc
32
+ tag_hash << tag.call(request)
33
+ else
34
+ tag_hash << tag
35
+ end
36
+ end
37
+ end
38
+
39
+ @logger.push_request_tags(tag_hash)
40
+ end
41
+
42
+ end
43
+
44
+ end
@@ -0,0 +1,55 @@
1
+ require 'logger'
2
+
3
+ module RailsLogstasher
4
+
5
+ # Based on the ActiveSupport::Logger (Formerly known as BufferedLogger)
6
+ class Logger < ::Logger
7
+ # Broadcasts logs to multiple loggers.
8
+ def self.broadcast(logger) # :nodoc:
9
+ Module.new do
10
+ define_method(:add) do |*args, &block|
11
+ logger.add(*args, &block)
12
+ super(*args, &block)
13
+ end
14
+
15
+ define_method(:<<) do |x|
16
+ logger << x
17
+ super(x)
18
+ end
19
+
20
+ define_method(:close) do
21
+ logger.close
22
+ super()
23
+ end
24
+
25
+ define_method(:progname=) do |name|
26
+ logger.progname = name
27
+ super(name)
28
+ end
29
+
30
+ define_method(:formatter=) do |formatter|
31
+ logger.formatter = formatter
32
+ super(formatter)
33
+ end
34
+
35
+ define_method(:level=) do |level|
36
+ logger.level = level
37
+ super(level)
38
+ end
39
+ end
40
+ end
41
+
42
+ def initialize(*args)
43
+ super
44
+ @formatter = SimpleFormatter.new
45
+ end
46
+
47
+ # Simple formatter which only displays the message.
48
+ class SimpleFormatter < ::Logger::Formatter
49
+ # This method is invoked when a log event occurs
50
+ def call(severity, timestamp, progname, msg)
51
+ "#{String === msg ? msg : msg.inspect}\n"
52
+ end
53
+ end
54
+ end
55
+ end
@@ -0,0 +1,53 @@
1
+ module RailsLogstasher
2
+
3
+ module Rack
4
+
5
+ class Logger
6
+
7
+ def initialize(app, tags = nil)
8
+ @app, @tags = app, tags.presence
9
+ end
10
+
11
+ def call(env)
12
+
13
+ t1 = Time.now
14
+ request = ActionDispatch::Request.new(env)
15
+
16
+ event = RailsLogstasher::Event.new(Rails.logger, true)
17
+ event.message = "#{request.request_method} #{request.filtered_path} for #{request.ip}"
18
+ event.fields['client_ip'] = request.ip
19
+ event.fields['method'] = request.request_method
20
+ event.fields['path'] = request.filtered_path
21
+ #TODO Should really move this into the base logger
22
+ event.source = "http://#{Socket.gethostname}#{request.filtered_path}"
23
+
24
+ event.add_tags_to_logger(request, @tags) if @tags
25
+
26
+ RailsLogstasher.log_entries[Thread.current] = event
27
+
28
+ status, headers, response = @app.call(env)
29
+ [status, headers, response]
30
+
31
+ ensure
32
+ if event
33
+ event.fields['total_duration'] = Time.now - t1
34
+ event.fields['status'] = status
35
+
36
+ ['rendering','sql'].each do |type|
37
+ if event.fields[type] && !event.fields[type].empty?
38
+ duration = event.fields[type].inject(0) {|result, local_event| result += local_event[:duration].to_f }
39
+ event.fields["#{type}_duration"] = duration
40
+ end
41
+ end
42
+
43
+ event.write(true)
44
+ end
45
+
46
+ RailsLogstasher.log_entries[Thread.current] = nil
47
+ end
48
+
49
+ end
50
+
51
+ end
52
+
53
+ end
@@ -0,0 +1,70 @@
1
+ require 'rails_logstasher/core_ext/object/blank'
2
+ require 'rails_logstasher/action_controller/log_subscriber'
3
+ require 'rails_logstasher/action_view/log_subscriber'
4
+ require 'rails_logstasher/active_record/log_subscriber' if defined?(ActiveRecord)
5
+ require 'rails_logstasher/active_resource/log_subscriber' if defined?(ActiveResource)
6
+
7
+ module RailsLogstasher
8
+
9
+ # Railtie to hook RailsLogstasher into Rails
10
+ #
11
+ # This Railtie hooks RailsLogstasher into Rails by adding middleware and loggers as well as
12
+ # adding a completely new set of LogSubscribers which parallel the default rails ones but
13
+ # are JSON based rather than string based
14
+ class Railtie < Rails::Railtie
15
+
16
+ initializer "rails_logstasher.swap_rack_logger_middleware" do |app|
17
+ app.middleware.swap(Rails::Rack::Logger, RailsLogstasher::Rack::Logger, app.config.log_tags)
18
+ end
19
+
20
+ # Silence the asset logger. This has to be done in a before_initialize block because
21
+ # the initializer is too late. (There might be a better part of the boot process for
22
+ # this, keep an eye out)
23
+ config.before_initialize do |app|
24
+ app.config.assets.logger = false
25
+
26
+ if app.config.logger.nil? && Rails.logger.class == ActiveSupport::TaggedLogging
27
+ raise IncompatibleLogger, "Please replace the default rails logger (See the " +
28
+ "Configuration section of the RailsLogstasher README)"
29
+ end
30
+
31
+ # Take the current logger and replace it with itself wrapped by the
32
+ # RailsLogstasher::TaggedLogging class
33
+ app.config.log_type = 'rails' unless app.config.respond_to? :log_type
34
+ app.config.logger = RailsLogstasher::TaggedLogging.new(app.config.logger, app.config.log_type)
35
+ end
36
+
37
+
38
+ # We need to do the following in an after_initialize block to make sure we get all the
39
+ # subscribers. Ideally rails would allow us the ability to stop the LogSubscribers from
40
+ # registering themselves using a config option.
41
+ config.after_initialize do
42
+
43
+ # Kludge the removal of the default LogSubscribers for the moment. We will use the rails_logstasher
44
+ # LogSubscribers (since they subscribe to the same hooks in the public methods) to create
45
+ # a list of hooks we want to unsubscribe current subscribers from.
46
+ modules = ["ActionController", "ActionView"]
47
+ modules << "ActiveRecord" if defined?(ActiveRecord)
48
+ modules << "ActiveResource" if defined?(ActiveResource)
49
+
50
+ notifier = ActiveSupport::Notifications.notifier
51
+
52
+ modules.each do |mod|
53
+ "RailsLogstasher::#{mod}::LogSubscriber".constantize.instance_methods(false).each do |method|
54
+ notifier.listeners_for("#{method}.#{mod.underscore}").each do |subscriber|
55
+ ActiveSupport::Notifications.unsubscribe subscriber
56
+ end
57
+ end
58
+ end
59
+
60
+ # We then subscribe using the rails_logstasher versions of the default rails LogSubscribers
61
+ RailsLogstasher::ActionController::LogSubscriber.attach_to :action_controller
62
+ RailsLogstasher::ActionView::LogSubscriber.attach_to :action_view
63
+ RailsLogstasher::ActiveRecord::LogSubscriber.attach_to :active_record if defined?(ActiveRecord)
64
+ RailsLogstasher::ActiveResource::LogSubscriber.attach_to :active_resource if defined?(ActiveResource)
65
+
66
+ end
67
+
68
+ end
69
+
70
+ end
@@ -0,0 +1,108 @@
1
+ require 'rails_logstasher/core_ext/object/blank'
2
+ require 'logger'
3
+ require 'rails_logstasher/logger'
4
+
5
+ module RailsLogstasher
6
+ # Wraps any standard Logger object to provide tagging capabilities.
7
+ #
8
+ # logger = RailsLogstasher::TaggedLogging.new(Logger.new(STDOUT))
9
+ # logger.tagged('BCX') { logger.info 'Stuff' } # Adds BCX to the @tags array and "Stuff" to the @message
10
+ # logger.tagged('BCX', "Jason") { logger.info 'Stuff' } # Adds 'BCX' and 'Jason' to the @tags array and "Stuff"
11
+ # to the @message
12
+ # logger.tagged('BCX') { logger.tagged('Jason') { logger.info 'Stuff' } } # Adds 'BCX' and 'Jason' to the @tags
13
+ # array and "Stuff" to the @message
14
+ #
15
+ # This is used by the default Rails.logger when the RailsLogstasher gem is added to a rails application
16
+ # to make it easy to stamp JSON logs with subdomains, request ids, and anything else
17
+ # to aid debugging of multi-user production applications.
18
+ module TaggedLogging
19
+ module Formatter # :nodoc:
20
+ # This method is invoked when a log event occurs.
21
+ def call(severity, timestamp, progname, msg)
22
+ @entry = nil
23
+ if msg.class == RailsLogstasher::Event
24
+ @entry = msg
25
+ else
26
+ @entry = RailsLogstasher::Event.new(Rails.logger)
27
+ @entry.message = msg
28
+ end
29
+ @entry.fields['severity'] = severity
30
+ @entry.type = @log_type
31
+ process_tags(current_tags)
32
+ process_tags(current_request_tags)
33
+ #TODO Should we do anything with progname? What about source?
34
+ super(severity, timestamp, progname, @entry.to_json)
35
+ end
36
+
37
+ def tagged(*tags)
38
+ new_tags = push_tags(*tags)
39
+ yield self
40
+ ensure
41
+ pop_tags(new_tags.size)
42
+ end
43
+
44
+ def push_tags(*tags)
45
+ tags.flatten.reject(&:blank?).tap do |new_tags|
46
+ current_tags.concat new_tags
47
+ end
48
+ end
49
+
50
+ def push_request_tags(tags)
51
+ Thread.current[:activesupport_tagged_logging_request_tags] = tags
52
+ end
53
+
54
+ def process_tags(tags)
55
+ tags.each do |tag|
56
+ if tag.class == Hash
57
+ tag.each_pair do |k,v|
58
+ @entry.fields[k] = v
59
+ end
60
+ else
61
+ @entry.tags << tag
62
+ end
63
+ end
64
+ end
65
+
66
+ def pop_tags(size = 1)
67
+ current_tags.pop size
68
+ end
69
+
70
+ def clear_tags!
71
+ current_request_tags.clear
72
+ current_tags.clear
73
+ end
74
+
75
+ def current_tags
76
+ Thread.current[:activesupport_tagged_logging_tags] ||= []
77
+ end
78
+
79
+ def current_request_tags
80
+ Thread.current[:activesupport_tagged_logging_request_tags] ||= []
81
+ end
82
+
83
+ def log_type=(log_type)
84
+ @log_type = log_type
85
+ end
86
+
87
+ end
88
+
89
+ def self.new(logger, log_type)
90
+ # Ensure we set a default formatter so we aren't extending nil!
91
+ logger.formatter ||= ActiveSupport::Logger::SimpleFormatter.new
92
+ logger.formatter.extend Formatter
93
+ logger.formatter.log_type = log_type
94
+ logger.extend(self)
95
+ end
96
+
97
+ delegate :push_tags, :push_request_tags, :pop_tags, :clear_tags!, :log_type=, to: :formatter
98
+
99
+ def tagged(*tags)
100
+ formatter.tagged(*tags) { yield self }
101
+ end
102
+
103
+ def flush
104
+ clear_tags!
105
+ super if defined?(super)
106
+ end
107
+ end
108
+ end
@@ -0,0 +1,3 @@
1
+ module RailsLogstasher
2
+ VERSION = "0.1.0"
3
+ end
@@ -0,0 +1,165 @@
1
+ require "active_support/log_subscriber/test_helper"
2
+ require "test_helper"
3
+
4
+ class ACLogSubscriberTest < ActionController::TestCase
5
+ tests LogSubscribersController
6
+ include ActiveSupport::LogSubscriber::TestHelper
7
+
8
+ def setup
9
+ super
10
+
11
+ @cache_path = File.expand_path('../temp/test_cache', File.dirname(__FILE__))
12
+ ActionController::Base.page_cache_directory = @cache_path
13
+ @controller.cache_store = :file_store, @cache_path
14
+
15
+ RailsLogstasher::ActionController::LogSubscriber.attach_to :action_controller
16
+ RailsLogstasher.log_entries[Thread.current] = LogStash::Event.new
17
+ @log_entry = RailsLogstasher.log_entries[Thread.current]
18
+ end
19
+
20
+ def teardown
21
+ super
22
+ ActiveSupport::LogSubscriber.log_subscribers.clear
23
+ FileUtils.rm_rf(@cache_path)
24
+ end
25
+
26
+ def set_logger(logger)
27
+ ActionController::Base.logger = logger
28
+ end
29
+
30
+ def test_start_processing
31
+ get :show, {:test => 'test'}
32
+ wait
33
+
34
+ assert_equal "LogSubscribersController", @log_entry.fields['controller']
35
+ assert_equal "show", @log_entry.fields['action']
36
+ assert_equal "html", @log_entry.fields['format']
37
+ end
38
+
39
+
40
+ def test_halted_callback
41
+ get :never_executed
42
+ wait
43
+
44
+ assert_equal ":redirector" ,@log_entry.fields['halted_callback']
45
+ end
46
+
47
+ def test_process_action
48
+ get :show
49
+ wait
50
+
51
+ assert_present @log_entry.fields['controller_duration']
52
+ end
53
+
54
+ def test_process_action_without_parameters
55
+ get :show
56
+ wait
57
+
58
+ assert_blank @log_entry.fields['parameters']
59
+ end
60
+
61
+ def test_process_action_with_parameters
62
+ get :show, :id => '10'
63
+ wait
64
+
65
+ assert_equal '10', @log_entry.fields['parameters']['id']
66
+ end
67
+
68
+ def test_process_action_with_wrapped_parameters
69
+ @request.env['CONTENT_TYPE'] = 'application/json'
70
+ post :show, :id => '10', :name => 'jose'
71
+ wait
72
+
73
+ assert_equal '10', @log_entry.fields['parameters']['id']
74
+ assert_equal 'jose', @log_entry.fields['parameters']['name']
75
+ end
76
+
77
+ def test_process_action_with_filter_parameters
78
+ @request.env["action_dispatch.parameter_filter"] = [:lifo, :amount]
79
+
80
+ get :show, :lifo => 'Pratik', :amount => '420', :step => '1'
81
+ wait
82
+
83
+ params = @log_entry.fields['parameters']
84
+ assert_equal '[FILTERED]', params['amount']
85
+ assert_equal '[FILTERED]', params['lifo']
86
+ assert_equal '1', params['step']
87
+ end
88
+
89
+ def test_redirect_to
90
+ get :redirector
91
+ wait
92
+
93
+ assert_equal 'http://foo.bar/', @log_entry.fields['redirect_to']
94
+ end
95
+
96
+
97
+ def test_send_data
98
+ get :data_sender
99
+ wait
100
+
101
+ assert_equal 'file.txt', @log_entry.fields['send_data']
102
+ assert_present @log_entry.fields['send_data_duration']
103
+ end
104
+
105
+
106
+ def test_send_file
107
+ get :file_sender
108
+ wait
109
+
110
+ assert_match 'test/dummy/public/favicon.ico', @log_entry.fields['send_file']
111
+ assert_present @log_entry.fields['send_file_duration']
112
+ end
113
+
114
+ def test_with_fragment_cache
115
+ @controller.config.perform_caching = true
116
+ get :with_fragment_cache
117
+ wait
118
+
119
+ assert_present @log_entry.fields['cache']
120
+
121
+ assert_match('Read fragment', @log_entry.fields['cache'].first['type'])
122
+ assert_match('views/foo', @log_entry.fields['cache'].first['key_or_path'])
123
+
124
+ assert_match('Write fragment', @log_entry.fields['cache'].last['type'])
125
+ assert_match('views/foo', @log_entry.fields['cache'].last['key_or_path'])
126
+ ensure
127
+ LogSubscribersController.config.perform_caching = true
128
+ end
129
+
130
+
131
+ def test_with_fragment_cache_and_percent_in_key
132
+ @controller.config.perform_caching = true
133
+ get :with_fragment_cache_and_percent_in_key
134
+ wait
135
+
136
+ assert_present @log_entry.fields['cache']
137
+
138
+ assert_match('Read fragment', @log_entry.fields['cache'].first['type'])
139
+ assert_match('views/foo', @log_entry.fields['cache'].first['key_or_path'])
140
+
141
+ assert_match('Write fragment', @log_entry.fields['cache'].last['type'])
142
+ assert_match('views/foo', @log_entry.fields['cache'].last['key_or_path'])
143
+ ensure
144
+ LogSubscribersController.config.perform_caching = true
145
+ end
146
+
147
+ =begin TODO Figure out why this last test fails.
148
+ def test_with_page_cache
149
+ @controller.config.perform_caching = true
150
+ get :with_page_cache
151
+ wait
152
+
153
+ assert_present @log_entry.fields['cache']
154
+
155
+ assert_match('Write page', @log_entry.fields['cache'][1]['type'])
156
+ assert_match('index.html', @log_entry.fields['cache'][1]['key_or_path'])
157
+ ensure
158
+ @controller.config.perform_caching = true
159
+ end
160
+ =end
161
+
162
+ def logs
163
+ @logs ||= @logger.logged(:info)
164
+ end
165
+ end