stasher 0.1.0
Sign up to get free protection for your applications and to get access to all the features.
- data/.gitattributes +11 -0
- data/.gitignore +17 -0
- data/.rspec +2 -0
- data/.ruby-gemset +1 -0
- data/Gemfile +15 -0
- data/Guardfile +11 -0
- data/LICENSE.txt +22 -0
- data/README.md +101 -0
- data/Rakefile +8 -0
- data/lib/stasher.rb +137 -0
- data/lib/stasher/current_scope.rb +21 -0
- data/lib/stasher/log_subscriber.rb +128 -0
- data/lib/stasher/logger.rb +40 -0
- data/lib/stasher/rails_ext/action_controller/metal/instrumentation.rb +31 -0
- data/lib/stasher/rails_ext/rack/logger.rb +24 -0
- data/lib/stasher/railtie.rb +17 -0
- data/lib/stasher/version.rb +3 -0
- data/spec/factories/payloads.rb +25 -0
- data/spec/lib/stasher/current_scope_spec.rb +42 -0
- data/spec/lib/stasher/log_subscriber_spec.rb +214 -0
- data/spec/lib/stasher/logger_spec.rb +90 -0
- data/spec/lib/stasher_spec.rb +246 -0
- data/spec/spec_helper.rb +64 -0
- data/spec/support/mock_logger.rb +19 -0
- data/stasher.gemspec +27 -0
- metadata +164 -0
data/.gitattributes
ADDED
data/.gitignore
ADDED
data/.rspec
ADDED
data/.ruby-gemset
ADDED
@@ -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
|
data/Guardfile
ADDED
@@ -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
|
data/LICENSE.txt
ADDED
@@ -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.
|
data/README.md
ADDED
@@ -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
|
data/Rakefile
ADDED
data/lib/stasher.rb
ADDED
@@ -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
|