stasher 0.1.0

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