yarder 0.0.1

Sign up to get free protection for your applications and to get access to all the features.
Files changed (77) hide show
  1. data/MIT-LICENSE +20 -0
  2. data/README.md +87 -0
  3. data/Rakefile +38 -0
  4. data/lib/tasks/yarder_tasks.rake +4 -0
  5. data/lib/yarder.rb +15 -0
  6. data/lib/yarder/action_controller/log_subscriber.rb +74 -0
  7. data/lib/yarder/action_view/log_subscriber.rb +31 -0
  8. data/lib/yarder/active_record/log_subscriber.rb +75 -0
  9. data/lib/yarder/active_resource/log_subscriber.rb +34 -0
  10. data/lib/yarder/configuration.rb +8 -0
  11. data/lib/yarder/core_ext/object/blank.rb +105 -0
  12. data/lib/yarder/logger.rb +55 -0
  13. data/lib/yarder/rack/logger.rb +72 -0
  14. data/lib/yarder/railtie.rb +69 -0
  15. data/lib/yarder/tagged_logging.rb +102 -0
  16. data/lib/yarder/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/dummy/README.rdoc +261 -0
  22. data/test/dummy/Rakefile +7 -0
  23. data/test/dummy/app/assets/javascripts/application.js +13 -0
  24. data/test/dummy/app/assets/javascripts/widgets.js +2 -0
  25. data/test/dummy/app/assets/stylesheets/application.css +13 -0
  26. data/test/dummy/app/assets/stylesheets/scaffold.css +56 -0
  27. data/test/dummy/app/assets/stylesheets/widgets.css +4 -0
  28. data/test/dummy/app/controllers/application_controller.rb +3 -0
  29. data/test/dummy/app/controllers/log_subscriber_controller.rb +56 -0
  30. data/test/dummy/app/controllers/widgets_controller.rb +83 -0
  31. data/test/dummy/app/helpers/application_helper.rb +2 -0
  32. data/test/dummy/app/helpers/widgets_helper.rb +2 -0
  33. data/test/dummy/app/models/widget.rb +3 -0
  34. data/test/dummy/app/views/customers/_customer.html.erb +1 -0
  35. data/test/dummy/app/views/good_customers/_good_customer.html.erb +1 -0
  36. data/test/dummy/app/views/layouts/application.html.erb +14 -0
  37. data/test/dummy/app/views/test/_customer.erb +1 -0
  38. data/test/dummy/app/views/test/hello_world.erb +1 -0
  39. data/test/dummy/app/views/widgets/_form.html.erb +17 -0
  40. data/test/dummy/app/views/widgets/edit.html.erb +6 -0
  41. data/test/dummy/app/views/widgets/index.html.erb +21 -0
  42. data/test/dummy/app/views/widgets/new.html.erb +5 -0
  43. data/test/dummy/app/views/widgets/show.html.erb +5 -0
  44. data/test/dummy/config.ru +4 -0
  45. data/test/dummy/config/application.rb +69 -0
  46. data/test/dummy/config/boot.rb +10 -0
  47. data/test/dummy/config/database.yml +22 -0
  48. data/test/dummy/config/environment.rb +5 -0
  49. data/test/dummy/config/environments/development.rb +31 -0
  50. data/test/dummy/config/environments/production.rb +64 -0
  51. data/test/dummy/config/environments/test.rb +35 -0
  52. data/test/dummy/config/initializers/backtrace_silencers.rb +7 -0
  53. data/test/dummy/config/initializers/inflections.rb +15 -0
  54. data/test/dummy/config/initializers/mime_types.rb +5 -0
  55. data/test/dummy/config/initializers/secret_token.rb +7 -0
  56. data/test/dummy/config/initializers/session_store.rb +8 -0
  57. data/test/dummy/config/initializers/wrap_parameters.rb +10 -0
  58. data/test/dummy/config/locales/en.yml +5 -0
  59. data/test/dummy/config/routes.rb +69 -0
  60. data/test/dummy/db/migrate/20120927084605_create_widgets.rb +8 -0
  61. data/test/dummy/db/schema.rb +16 -0
  62. data/test/dummy/log/development.log +19 -0
  63. data/test/dummy/log/test.log +10694 -0
  64. data/test/dummy/public/404.html +26 -0
  65. data/test/dummy/public/422.html +26 -0
  66. data/test/dummy/public/500.html +25 -0
  67. data/test/dummy/public/favicon.ico +0 -0
  68. data/test/dummy/script/rails +6 -0
  69. data/test/logger_test.rb +126 -0
  70. data/test/rack/logger_test.rb +68 -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 +31 -0
  76. data/test/yarder_test.rb +7 -0
  77. metadata +236 -0
@@ -0,0 +1,55 @@
1
+ require 'logger'
2
+
3
+ module Yarder
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,72 @@
1
+ module Yarder
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 = LogStash::Event.new
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
+ event.type = "rails_json_log"
24
+
25
+ add_tags_to_logger(request) if @tags
26
+
27
+ Yarder.log_entries[Thread.current] = event
28
+
29
+ status, headers, response = @app.call(env)
30
+ [status, headers, response]
31
+
32
+ ensure
33
+ if event
34
+ event.fields['total_duration'] = Time.now - t1
35
+ event.fields['status'] = status
36
+
37
+ ['rendering','sql'].each do |type|
38
+ if event.fields[type] && !event.fields[type].empty?
39
+ duration = event.fields[type].inject(0) {|result, event| result += event[:duration].to_f }
40
+ event.fields["#{type}_duration"] = duration
41
+ end
42
+ end
43
+
44
+ Rails.logger.info event
45
+ end
46
+
47
+ Yarder.log_entries[Thread.current] = nil
48
+ end
49
+
50
+ def add_tags_to_logger(request)
51
+ tags = []
52
+ if @tags
53
+ @tags.each do |tag|
54
+ case tag
55
+ when Symbol
56
+ tags << {tag.to_s => request.send(tag) }
57
+ when Proc
58
+ tags << tag.call(request)
59
+ else
60
+ tags << tag
61
+ end
62
+ end
63
+ end
64
+
65
+ Rails.logger.push_request_tags(tags)
66
+ end
67
+
68
+ end
69
+
70
+ end
71
+
72
+ end
@@ -0,0 +1,69 @@
1
+ require 'yarder/core_ext/object/blank'
2
+ require 'yarder/action_controller/log_subscriber'
3
+ require 'yarder/action_view/log_subscriber'
4
+ require 'yarder/active_record/log_subscriber' if defined?(ActiveRecord)
5
+ require 'yarder/active_resource/log_subscriber' if defined?(ActiveResource)
6
+
7
+ module Yarder
8
+
9
+ # Railtie to hook Yarder into Rails
10
+ #
11
+ # This Railtie hooks Yarder 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 "yarder.swap_rack_logger_middleware" do |app|
17
+ app.middleware.swap(Rails::Rack::Logger, Yarder::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 Yarder README)"
29
+ end
30
+
31
+ # Take the current logger and replace it with itself wrapped by the
32
+ # Yarder::TaggedLogging class
33
+ app.config.logger = Yarder::TaggedLogging.new(app.config.logger)
34
+ end
35
+
36
+
37
+ # We need to do the following in an after_initialize block to make sure we get all the
38
+ # subscribers. Ideally rails would allow us the ability to stop the LogSubscribers from
39
+ # registering themselves using a config option.
40
+ config.after_initialize do
41
+
42
+ # Kludge the removal of the default LogSubscribers for the moment. We will use the yarder
43
+ # LogSubscribers (since they subscribe to the same hooks in the public methods) to create
44
+ # a list of hooks we want to unsubscribe current subscribers from.
45
+ modules = ["ActionController", "ActionView"]
46
+ modules << "ActiveRecord" if defined?(ActiveRecord)
47
+ modules << "ActiveResource" if defined?(ActiveResource)
48
+
49
+ notifier = ActiveSupport::Notifications.notifier
50
+
51
+ modules.each do |mod|
52
+ "Yarder::#{mod}::LogSubscriber".constantize.instance_methods(false).each do |method|
53
+ notifier.listeners_for("#{method}.#{mod.underscore}").each do |subscriber|
54
+ ActiveSupport::Notifications.unsubscribe subscriber
55
+ end
56
+ end
57
+ end
58
+
59
+ # We then subscribe using the yarder versions of the default rails LogSubscribers
60
+ Yarder::ActionController::LogSubscriber.attach_to :action_controller
61
+ Yarder::ActionView::LogSubscriber.attach_to :action_view
62
+ Yarder::ActiveRecord::LogSubscriber.attach_to :active_record if defined?(ActiveRecord)
63
+ Yarder::ActiveResource::LogSubscriber.attach_to :active_resource if defined?(ActiveResource)
64
+
65
+ end
66
+
67
+ end
68
+
69
+ end
@@ -0,0 +1,102 @@
1
+ require 'yarder/core_ext/object/blank'
2
+ require 'logger'
3
+ require 'yarder/logger'
4
+
5
+ module Yarder
6
+ # Wraps any standard Logger object to provide tagging capabilities.
7
+ #
8
+ # logger = Yarder::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 Yarder 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 == LogStash::Event
24
+ @entry = msg
25
+ else
26
+ @entry = LogStash::Event.new
27
+ @entry.message = msg
28
+ end
29
+ @entry.fields['severity'] = severity
30
+ process_tags(current_tags)
31
+ process_tags(current_request_tags)
32
+ #TODO Should we do anything with progname? What about source?
33
+ super(severity, timestamp, progname, @entry.to_json)
34
+ end
35
+
36
+ def tagged(*tags)
37
+ new_tags = push_tags(*tags)
38
+ yield self
39
+ ensure
40
+ pop_tags(new_tags.size)
41
+ end
42
+
43
+ def push_tags(*tags)
44
+ tags.flatten.reject(&:blank?).tap do |new_tags|
45
+ current_tags.concat new_tags
46
+ end
47
+ end
48
+
49
+ def push_request_tags(tags)
50
+ Thread.current[:activesupport_tagged_logging_request_tags] = tags
51
+ end
52
+
53
+ def process_tags(tags)
54
+ tags.each do |tag|
55
+ if tag.class == Hash
56
+ tag.each_pair do |k,v|
57
+ @entry.fields[k] = v
58
+ end
59
+ else
60
+ @entry.tags << tag
61
+ end
62
+ end
63
+ end
64
+
65
+ def pop_tags(size = 1)
66
+ current_tags.pop size
67
+ end
68
+
69
+ def clear_tags!
70
+ current_request_tags.clear
71
+ current_tags.clear
72
+ end
73
+
74
+ def current_tags
75
+ Thread.current[:activesupport_tagged_logging_tags] ||= []
76
+ end
77
+
78
+ def current_request_tags
79
+ Thread.current[:activesupport_tagged_logging_request_tags] ||= []
80
+ end
81
+
82
+ end
83
+
84
+ def self.new(logger)
85
+ # Ensure we set a default formatter so we aren't extending nil!
86
+ logger.formatter ||= ActiveSupport::Logger::SimpleFormatter.new
87
+ logger.formatter.extend Formatter
88
+ logger.extend(self)
89
+ end
90
+
91
+ delegate :push_tags, :push_request_tags, :pop_tags, :clear_tags!, to: :formatter
92
+
93
+ def tagged(*tags)
94
+ formatter.tagged(*tags) { yield self }
95
+ end
96
+
97
+ def flush
98
+ clear_tags!
99
+ super if defined?(super)
100
+ end
101
+ end
102
+ end
@@ -0,0 +1,3 @@
1
+ module Yarder
2
+ VERSION = "0.0.1"
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
+ Yarder::ActionController::LogSubscriber.attach_to :action_controller
16
+ Yarder.log_entries[Thread.current] = LogStash::Event.new
17
+ @log_entry = Yarder.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