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.
- 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
|