stasher 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.
@@ -0,0 +1,11 @@
1
+ * text=auto eol=lf
2
+
3
+ *.cmd text eol=crlf
4
+
5
+ *.png binary
6
+ *.jpg binary
7
+ *.gif binary
8
+ *.ico binary
9
+ *.ttf binary
10
+ *.woff binary
11
+ *.eot binary
@@ -0,0 +1,17 @@
1
+ *.gem
2
+ *.rbc
3
+ .bundle
4
+ .config
5
+ .yardoc
6
+ Gemfile.lock
7
+ InstalledFiles
8
+ _yardoc
9
+ coverage
10
+ doc/
11
+ lib/bundler/man
12
+ pkg
13
+ rdoc
14
+ spec/reports
15
+ test/tmp
16
+ test/version_tmp
17
+ tmp
data/.rspec ADDED
@@ -0,0 +1,2 @@
1
+ --color
2
+ --format documentation
@@ -0,0 +1 @@
1
+ stasher
data/Gemfile ADDED
@@ -0,0 +1,15 @@
1
+ source 'https://rubygems.org'
2
+
3
+ # Specify your gem's dependencies in stasher.gemspec
4
+ gemspec
5
+
6
+ group :test do
7
+ gem 'rb-fsevent'
8
+ gem 'guard'
9
+ gem 'guard-rspec'
10
+ gem 'growl'
11
+ gem 'factory_girl'
12
+ gem 'simplecov', :platforms => :mri_19, :require => false
13
+ gem 'rcov', :platforms => :mri_18
14
+ gem 'rails', "~> #{ENV["RAILS_VERSION"] || "3.2.0"}"
15
+ end
@@ -0,0 +1,11 @@
1
+ # A sample Guardfile
2
+ # More info at https://github.com/guard/guard#readme
3
+ interactor :simple
4
+
5
+ guard 'rspec' do
6
+ watch(%r{^spec/.+_spec\.rb$})
7
+ watch(%r{^spec/factories/.+\.rb$}) { "spec" }
8
+ watch(%r{^spec/support/.+\.rb$}) { "spec" }
9
+ watch(%r{^lib/(.+)\.rb$}) { |m| "spec/lib/#{m[1]}_spec.rb" }
10
+ watch('spec/spec_helper.rb') { "spec" }
11
+ end
@@ -0,0 +1,22 @@
1
+ Copyright (c) 2013 Chris Micacchi
2
+
3
+ MIT License
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining
6
+ a copy of this software and associated documentation files (the
7
+ "Software"), to deal in the Software without restriction, including
8
+ without limitation the rights to use, copy, modify, merge, publish,
9
+ distribute, sublicense, and/or sell copies of the Software, and to
10
+ permit persons to whom the Software is furnished to do so, subject to
11
+ the following conditions:
12
+
13
+ The above copyright notice and this permission notice shall be
14
+ included in all copies or substantial portions of the Software.
15
+
16
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
17
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
18
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
19
+ NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
20
+ LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
21
+ OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
22
+ WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
@@ -0,0 +1,101 @@
1
+ # Stasher
2
+
3
+ This gem is a heavy modification of [Logstasher](https://github.com/shadabahmed/logstasher), which was
4
+ inspired from [LogRage](https://github.com/roidrage/lograge). It adds the same request logging for logstash as
5
+ Logstasher, but separates out request and response log entries, and adds a modified Ruby Logger instance to allow
6
+ you to send all of your logging to logstash.
7
+
8
+ ## About stasher
9
+
10
+ This gem logs to a separate log file named `logstash_<environment>.log`. It provides two facilities:
11
+ * Request and response logging (ala Logstasher and LogRage)
12
+ * Redirection of the Rails logger, with request-scoped parameters
13
+
14
+ Before **stasher** :
15
+
16
+ ```
17
+ Started GET "/login" for 10.109.10.135 at 2013-04-30 08:59:01 -0400
18
+ Processing by SessionsController#new as HTML
19
+ Rendered sessions/new.html.haml within layouts/application (4.3ms)
20
+ Rendered shared/_javascript.html.haml (0.6ms)
21
+ Rendered shared/_flashes.html.haml (0.2ms)
22
+ Rendered shared/_header.html.haml (52.9ms)
23
+ Rendered shared/_title.html.haml (0.2ms)
24
+ Rendered shared/_footer.html.haml (0.2ms)
25
+ Banner Load SELECT `banners`.* FROM `banners` WHERE `banner`.`active` = 1 ORDER BY created_at DESC
26
+ Found 3 banners to display on the login page
27
+ Completed 200 OK in 532ms (Views: 62.4ms | ActiveRecord: 0.0ms | ND API: 0.0ms)
28
+ ```
29
+
30
+ After **stasher**:
31
+
32
+ ```
33
+ {"@source":"rails://localhost/my-app","@tags":["request"],"@fields":{"method":"GET","path":"/login","format":"html","controller":"sessions"
34
+ ,"action":"login","ip":"127.0.0.1",params:{},"uuid":"e81ecd178ed3b591099f4d489760dfb6","user":"shadab_ahmed@abc.com",
35
+ "site":"internal"},"@timestamp":"2013-04-30T13:00:46.354500+00:00"}
36
+ {"@source":"rails://localhost/my-app","@tags":["sql"],"@fields":{"name":"Banner Load","sql":"SELECT `banners`.* FROM `banners` WHERE `banner`.`active` = 1 ORDER BY created_at DESC","uuid":"e81ecd178ed3b591099f4d489760dfb6"},"@timestamp":"2013-04-30T13:00:46.362300+00:00"}
37
+ {"@source":"rails://localhost/my-app","@tags":["log","debug"],"@fields":{"severity":"DEBUG","uuid":"e81ecd178ed3b591099f4d489760dfb6"},"@message":"Found 3 banners to display on the login page","@timestamp":"2013-04-30T13:00:46.353400+00:00"}
38
+ {"@source":"rails://localhost/my-app","@tags":["response"],"@fields":{"method":"GET","path":"/login","format":"html","controller":"sessions"
39
+ ,"action":"login","status":200,"duration":28.34,"view":25.96,"db":0.88,"ip":"127.0.0.1","uuid":"e81ecd178ed3b591099f4d489760dfb6","user":"shadab_ahmed@abc.com",
40
+ "site":"internal"},"@timestamp":"2013-04-30T13:00:46.354500+00:00"}
41
+ ```
42
+
43
+ By default, the older format rails request logs are disabled, though you can enable them.
44
+
45
+ All events logged within a Rack request will include the request's UUID, allowing you to follow individual requests through the logs.
46
+
47
+ ## Installation
48
+
49
+ In your Gemfile:
50
+
51
+ gem 'stasher'
52
+
53
+ ### Configure your `<environment>.rb` e.g. `development.rb`
54
+
55
+ # Enable the logstasher logs for the current environment and set the log level
56
+ config.stasher.enabled = true
57
+ config.stasher.log_level = :debug
58
+
59
+ # This line is optional if you do not want to suppress app logs in your <environment>.log
60
+ # config.stasher.suppress_app_log = false
61
+
62
+ # This line causes the Rails logger to be redirected to logstash as well
63
+ config.stasher.redirect_logger = true
64
+
65
+ # To prevent logging of SQL into logstash, remove :active_record from this line
66
+ config.stasher.attach_to = [ :action_controller, :active_record ]
67
+
68
+ ## Adding custom fields to the log
69
+
70
+ Since some fields are very specific to your application for e.g. *user_name*, so it is left
71
+ up to you to add them. Here's how to add those fields to the logs:
72
+
73
+ # Create a file - config/initializers/stasher.rb
74
+
75
+ if Stasher.enabled
76
+ Stasher.add_custom_fields do |fields|
77
+ # This block is run in application_controller context,
78
+ # so you have access to all controller methods
79
+ fields[:user] = current_user && current_user.mail
80
+ fields[:site] = request.path =~ /^\/api/ ? 'api' : 'user'
81
+ end
82
+ end
83
+
84
+ ## Versions
85
+ All versions require Rails 3.2.x and higher and Ruby 1.9.2+. This code has not been tested on Rails 4 and Ruby 2.0
86
+
87
+ ## Development
88
+ - Run tests - `rake`
89
+ - Generate test coverage report - `rake coverage`. Coverage report path - coverage/index.html
90
+
91
+ ## License
92
+
93
+ Released under MIT license.
94
+
95
+ ## Contributing
96
+
97
+ 1. Fork it
98
+ 2. Create your feature branch (`git checkout -b my-new-feature`)
99
+ 3. Commit your changes (`git commit -am 'Add some feature'`)
100
+ 4. Push to the branch (`git push origin my-new-feature`)
101
+ 5. Create new Pull Request
@@ -0,0 +1,8 @@
1
+ require "bundler/gem_tasks"
2
+ require 'rspec/core/rake_task'
3
+
4
+ task :default => :spec
5
+
6
+ RSpec::Core::RakeTask.new('spec') do |spec|
7
+ spec.pattern = "./spec/**/*_spec.rb"
8
+ end
@@ -0,0 +1,137 @@
1
+ require "stasher/version"
2
+ require 'stasher/log_subscriber'
3
+ require 'stasher/current_scope'
4
+ require 'stasher/logger'
5
+ require 'active_support/core_ext/module/attribute_accessors'
6
+ require 'active_support/core_ext/string/inflections'
7
+ require 'active_support/ordered_options'
8
+
9
+ module Stasher
10
+ # Logger for the logstash logs
11
+ mattr_accessor :logger, :enabled, :source
12
+
13
+ def self.remove_existing_log_subscriptions
14
+ ActiveSupport::LogSubscriber.log_subscribers.each do |subscriber|
15
+ case subscriber
16
+ when ActionView::LogSubscriber
17
+ unsubscribe(:action_view, subscriber)
18
+ when ActiveRecord::LogSubscriber
19
+ unsubscribe(:active_record, subscriber)
20
+ when ActionController::LogSubscriber
21
+ unsubscribe(:action_controller, subscriber)
22
+ end
23
+ end
24
+ end
25
+
26
+ def self.unsubscribe(component, subscriber)
27
+ events = subscriber.public_methods(false).reject{ |method| method.to_s == 'call' }
28
+ events.each do |event|
29
+ ActiveSupport::Notifications.notifier.listeners_for("#{event}.#{component}").each do |listener|
30
+ if listener.instance_variable_get('@delegate') == subscriber
31
+ ActiveSupport::Notifications.unsubscribe listener
32
+ end
33
+ end
34
+ end
35
+ end
36
+
37
+ def self.add_default_fields_to_scope(scope, request)
38
+ scope[:uuid] = request.uuid
39
+ end
40
+
41
+ def self.add_custom_fields(&block)
42
+ ActionController::Metal.send(:define_method, :stasher_add_custom_fields_to_scope, &block)
43
+ end
44
+
45
+ def self.setup(app)
46
+ app.config.action_dispatch.rack_cache[:verbose] = false if app.config.action_dispatch.rack_cache
47
+
48
+ # Compose source
49
+ self.source = "rails://#{hostname}/#{app.class.name.deconstantize.underscore}"
50
+
51
+ # Initialize & set up instrumentation
52
+ require 'stasher/rails_ext/action_controller/metal/instrumentation'
53
+ require 'logstash/event'
54
+ self.suppress_app_logs(app) if app.config.stasher.suppress_app_log
55
+
56
+ # Redirect Rails' logger if requested
57
+ Rails.logger = Stasher::Logger.new if app.config.stasher.redirect_logger
58
+
59
+ # Subscribe to configured events
60
+ app.config.stasher.attach_to.each do |target|
61
+ Stasher::LogSubscriber.attach_to target
62
+ end
63
+
64
+ # Initialize internal logger
65
+ self.logger = app.config.stasher.logger || Logger.new("#{Rails.root}/log/logstash_#{Rails.env}.log")
66
+ level = ::Logger.const_get(app.config.stasher.log_level.to_s.upcase) if app.config.stasher.log_level
67
+ self.logger.level = level || Logger::WARN
68
+
69
+ self.enabled = true
70
+ end
71
+
72
+ def self.suppress_app_logs(app)
73
+ require 'stasher/rails_ext/rack/logger'
74
+ Stasher.remove_existing_log_subscriptions
75
+
76
+ # Disable ANSI colorization
77
+ app.config.colorize_logging = false
78
+ end
79
+
80
+ def self.format_exception(type_name, message, backtrace)
81
+ {
82
+ :exception => {
83
+ :name => type_name,
84
+ :message => message,
85
+ :backtrace => backtrace
86
+ }
87
+ }
88
+ end
89
+
90
+ def self.log(severity, msg)
91
+ if self.logger && self.logger.send("#{severity.to_s.downcase}?")
92
+ data = {
93
+ :severity => severity.upcase
94
+ }
95
+ tags = ['log']
96
+
97
+ if msg.is_a? Exception
98
+ data.merge! self.format_exception(msg.class.name, msg.message, msg.backtrace.join("\n"))
99
+ msg = "#{msg.class.name}: #{msg.message}"
100
+ tags << 'exception'
101
+ else
102
+ # Strip ANSI codes from the message
103
+ msg.gsub!(/\u001B\[[0-9;]+m/, '')
104
+ end
105
+
106
+ return true if msg.empty?
107
+ data.merge! CurrentScope.fields
108
+
109
+ tags << severity.downcase
110
+
111
+ event = LogStash::Event.new(
112
+ '@fields' => data,
113
+ '@tags' => tags,
114
+ '@message' => msg,
115
+ '@source' => Stasher.source)
116
+ self.logger << event.to_json + "\n"
117
+ end
118
+ end
119
+
120
+ def self.hostname
121
+ require 'socket'
122
+
123
+ Socket.gethostname
124
+ end
125
+
126
+ class << self
127
+ %w( fatal error warn info debug unknown ).each do |severity|
128
+ eval <<-EOM, nil, __FILE__, __LINE__ + 1
129
+ def #{severity}(msg)
130
+ self.log(:#{severity}, msg)
131
+ end
132
+ EOM
133
+ end
134
+ end
135
+ end
136
+
137
+ require 'stasher/railtie' if defined?(Rails)
@@ -0,0 +1,21 @@
1
+ module Stasher
2
+ module CurrentScope
3
+ ##
4
+ # Gets the hash of fields in the current scope
5
+ def self.fields
6
+ Thread.current[:stasher_fields] ||= {}
7
+ end
8
+
9
+ ##
10
+ # Gets the hash of fields in the current scope
11
+ def self.fields=(values)
12
+ Thread.current[:stasher_fields] = values
13
+ end
14
+
15
+ ##
16
+ # Clears the current scope
17
+ def self.clear!
18
+ Thread.current[:stasher_fields] = nil
19
+ end
20
+ end
21
+ end
@@ -0,0 +1,128 @@
1
+ require 'active_support/core_ext/class/attribute'
2
+ require 'active_support/log_subscriber'
3
+
4
+ module Stasher
5
+ class LogSubscriber < ActiveSupport::LogSubscriber
6
+ def start_processing(ev)
7
+ # Initialize the scope at the start of the request
8
+ payload = ev.payload
9
+
10
+ data = extract_request(payload)
11
+ data.merge! extract_current_scope
12
+
13
+ log_event 'request', data
14
+ end
15
+
16
+ def process_action(ev)
17
+ payload = ev.payload
18
+
19
+ data = extract_request(payload)
20
+ data.merge! extract_status(payload)
21
+ data.merge! runtimes(ev)
22
+ data.merge! extract_exception(payload)
23
+ data.merge! extract_current_scope
24
+
25
+ log_event 'response', data do |event|
26
+ event.tags << 'exception' if payload[:exception]
27
+ end
28
+
29
+ # Clear the scope at the end of the request
30
+ Stasher::CurrentScope.clear!
31
+ end
32
+
33
+ def sql(ev)
34
+ payload = ev.payload
35
+
36
+ return if 'SCHEMA' == payload[:name]
37
+ return if payload[:name].blank?
38
+ return if payload[:name] =~ /ActiveRecord::SessionStore/
39
+
40
+ data = extract_sql(payload)
41
+ data.merge! runtimes(ev)
42
+ data.merge! extract_current_scope
43
+
44
+ log_event 'sql', data
45
+ end
46
+
47
+ def redirect_to(ev)
48
+ Stasher::CurrentScope.fields[:location] = ev.payload[:location]
49
+ end
50
+
51
+ private
52
+
53
+ def log_event(type, data)
54
+ event = LogStash::Event.new('@fields' => data, '@tags' => [type], '@source' => Stasher.source)
55
+ yield(event) if block_given?
56
+ Stasher.logger << event.to_json + "\n"
57
+ end
58
+
59
+ def extract_sql(payload)
60
+ {
61
+ :name => payload[:name],
62
+ :sql => payload[:sql].squeeze(' '),
63
+ }
64
+ end
65
+
66
+ def extract_request(payload)
67
+ {
68
+ :method => payload[:method],
69
+ :ip => payload[:ip],
70
+ :params => extract_parms(payload),
71
+ :path => extract_path(payload),
72
+ :format => extract_format(payload),
73
+ :controller => payload[:params]['controller'],
74
+ :action => payload[:params]['action']
75
+ }
76
+ end
77
+
78
+ def extract_parms(payload)
79
+ payload[:params].except(*ActionController::LogSubscriber::INTERNAL_PARAMS) if payload.include?(:params)
80
+ end
81
+
82
+ def extract_path(payload)
83
+ payload[:path].split("?").first
84
+ end
85
+
86
+ def extract_format(payload)
87
+ if ::ActionPack::VERSION::MAJOR == 3 && ::ActionPack::VERSION::MINOR == 0
88
+ payload[:formats].first
89
+ else
90
+ payload[:format]
91
+ end
92
+ end
93
+
94
+ def extract_status(payload)
95
+ if payload[:status]
96
+ { :status => payload[:status].to_i }
97
+ else
98
+ { :status => 0 }
99
+ end
100
+ end
101
+
102
+ def runtimes(event)
103
+ {
104
+ :duration => event.duration,
105
+ :view => event.payload[:view_runtime],
106
+ :db => event.payload[:db_runtime]
107
+ }.inject({}) do |runtimes, (name, runtime)|
108
+ runtimes[name] = runtime.to_f.round(2) if runtime
109
+ runtimes
110
+ end
111
+ end
112
+
113
+ def extract_current_scope
114
+ CurrentScope.fields
115
+ end
116
+
117
+ # Monkey patching to enable exception logging
118
+ def extract_exception(payload)
119
+ if payload[:exception]
120
+ exception, message = payload[:exception]
121
+
122
+ Stasher.format_exception(exception, message, $!.backtrace.join("\n"))
123
+ else
124
+ {}
125
+ end
126
+ end
127
+ end
128
+ end