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
@@ -0,0 +1,40 @@
|
|
1
|
+
require 'logger'
|
2
|
+
|
3
|
+
module Stasher
|
4
|
+
class Logger < ::Logger
|
5
|
+
def initialize(device = nil)
|
6
|
+
super(device)
|
7
|
+
end
|
8
|
+
|
9
|
+
def add(severity, message = nil, progname = nil, &block)
|
10
|
+
severity ||= UNKNOWN
|
11
|
+
if severity < @level
|
12
|
+
return true
|
13
|
+
end
|
14
|
+
|
15
|
+
progname ||= @progname
|
16
|
+
if message.nil?
|
17
|
+
if block_given?
|
18
|
+
message = yield
|
19
|
+
else
|
20
|
+
message = progname
|
21
|
+
progname = @progname
|
22
|
+
end
|
23
|
+
end
|
24
|
+
|
25
|
+
if message.is_a? String
|
26
|
+
message = format_message(severity, Time.now, progname, message).chomp
|
27
|
+
end
|
28
|
+
|
29
|
+
severity = format_severity(severity)
|
30
|
+
|
31
|
+
Stasher.log severity, message
|
32
|
+
|
33
|
+
true
|
34
|
+
end
|
35
|
+
|
36
|
+
private
|
37
|
+
|
38
|
+
|
39
|
+
end
|
40
|
+
end
|
@@ -0,0 +1,31 @@
|
|
1
|
+
module ActionController
|
2
|
+
module Instrumentation
|
3
|
+
def process_action(*args)
|
4
|
+
raw_payload = {
|
5
|
+
:controller => self.class.name,
|
6
|
+
:action => self.action_name,
|
7
|
+
:params => request.filtered_parameters,
|
8
|
+
:ip => request.remote_ip,
|
9
|
+
:format => request.format.try(:ref),
|
10
|
+
:method => request.method,
|
11
|
+
:path => (request.fullpath rescue "unknown")
|
12
|
+
}
|
13
|
+
|
14
|
+
Stasher.add_default_fields_to_scope(Stasher::CurrentScope.fields, request)
|
15
|
+
|
16
|
+
if self.respond_to?(:stasher_add_custom_fields_to_scope)
|
17
|
+
stasher_add_custom_fields_to_scope(Stasher::CurrentScope.fields)
|
18
|
+
end
|
19
|
+
|
20
|
+
ActiveSupport::Notifications.instrument("start_processing.action_controller", raw_payload.dup)
|
21
|
+
|
22
|
+
ActiveSupport::Notifications.instrument("process_action.action_controller", raw_payload) do |payload|
|
23
|
+
result = super
|
24
|
+
payload[:status] = response.status
|
25
|
+
append_info_to_payload(payload)
|
26
|
+
result
|
27
|
+
end
|
28
|
+
end
|
29
|
+
|
30
|
+
end
|
31
|
+
end
|
@@ -0,0 +1,24 @@
|
|
1
|
+
require 'rails/rack/logger'
|
2
|
+
|
3
|
+
module Rails
|
4
|
+
module Rack
|
5
|
+
# Overwrites defaults of Rails::Rack::Logger that cause
|
6
|
+
# unnecessary logging.
|
7
|
+
# This effectively removes the log lines from the log
|
8
|
+
# that say:
|
9
|
+
# Started GET / for 192.168.2.1...
|
10
|
+
class Logger
|
11
|
+
# Overwrites Rails 3.2 code that logs new requests
|
12
|
+
def call_app(*args)
|
13
|
+
env = args.last
|
14
|
+
@app.call(env)
|
15
|
+
ensure
|
16
|
+
ActiveSupport::LogSubscriber.flush_all!
|
17
|
+
end
|
18
|
+
|
19
|
+
# Overwrites Rails 3.0/3.1 code that logs new requests
|
20
|
+
def before_dispatch(env)
|
21
|
+
end
|
22
|
+
end
|
23
|
+
end
|
24
|
+
end
|
@@ -0,0 +1,17 @@
|
|
1
|
+
require 'rails/railtie'
|
2
|
+
require 'action_view/log_subscriber'
|
3
|
+
require 'action_controller/log_subscriber'
|
4
|
+
|
5
|
+
module Stasher
|
6
|
+
class Railtie < Rails::Railtie
|
7
|
+
config.stasher = ActiveSupport::OrderedOptions.new
|
8
|
+
config.stasher.enabled = false
|
9
|
+
config.stasher.suppress_app_log = true
|
10
|
+
config.stasher.redirect_logger = false
|
11
|
+
config.stasher.attach_to = [ :action_controller, :active_record]
|
12
|
+
|
13
|
+
initializer 'stasher' do |app|
|
14
|
+
Stasher.setup(app) if app.config.stasher.enabled
|
15
|
+
end
|
16
|
+
end
|
17
|
+
end
|
@@ -0,0 +1,25 @@
|
|
1
|
+
FactoryGirl.define do
|
2
|
+
factory :actioncontroller_payload, class: Hash do
|
3
|
+
skip_create
|
4
|
+
|
5
|
+
status 200
|
6
|
+
format 'application/json'
|
7
|
+
method 'GET'
|
8
|
+
path '/home?foo=bar'
|
9
|
+
ip "127.0.0.1"
|
10
|
+
params { {:controller => 'home', :action => 'index', 'foo' => 'bar' }.with_indifferent_access }
|
11
|
+
db_runtime 0.02
|
12
|
+
view_runtime 0.01
|
13
|
+
|
14
|
+
initialize_with { attributes }
|
15
|
+
end
|
16
|
+
|
17
|
+
factory :activerecord_sql_payload, class: Hash do
|
18
|
+
skip_create
|
19
|
+
|
20
|
+
sql "SELECT * FROM `users` WHERE `users`.`id` = 5"
|
21
|
+
name 'User Load'
|
22
|
+
|
23
|
+
initialize_with { attributes }
|
24
|
+
end
|
25
|
+
end
|
@@ -0,0 +1,42 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
|
3
|
+
describe Stasher::CurrentScope do
|
4
|
+
describe '.clear!' do
|
5
|
+
before :each do
|
6
|
+
Stasher::CurrentScope.fields[:foo] = 'bar'
|
7
|
+
Stasher::CurrentScope.fields[:baz] = { :bat => "man" }
|
8
|
+
end
|
9
|
+
|
10
|
+
it "removes all existing fields" do
|
11
|
+
Stasher::CurrentScope.clear!
|
12
|
+
|
13
|
+
Stasher::CurrentScope.fields.should == {}
|
14
|
+
end
|
15
|
+
end
|
16
|
+
|
17
|
+
describe ".fields" do
|
18
|
+
before :each do
|
19
|
+
Stasher::CurrentScope.fields = { :foo => "bar" }
|
20
|
+
end
|
21
|
+
|
22
|
+
it "can retrive a value" do
|
23
|
+
Stasher::CurrentScope.fields[:foo].should == "bar"
|
24
|
+
end
|
25
|
+
end
|
26
|
+
|
27
|
+
describe ".fields=" do
|
28
|
+
it "can assign all fields at once" do
|
29
|
+
Stasher::CurrentScope.fields = { :foo => "bar" }
|
30
|
+
|
31
|
+
Stasher::CurrentScope.fields[:foo].should == "bar"
|
32
|
+
end
|
33
|
+
|
34
|
+
it "overwrites exisitng fields" do
|
35
|
+
Stasher::CurrentScope.fields[:baz] = { :bat => "man" }
|
36
|
+
|
37
|
+
Stasher::CurrentScope.fields = { :foo => "bar" }
|
38
|
+
|
39
|
+
Stasher::CurrentScope.fields.should == { :foo => "bar" }
|
40
|
+
end
|
41
|
+
end
|
42
|
+
end
|
@@ -0,0 +1,214 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
|
3
|
+
describe Stasher::LogSubscriber do
|
4
|
+
let(:logger) { MockLogger.new }
|
5
|
+
|
6
|
+
before :each do
|
7
|
+
Stasher.logger = logger
|
8
|
+
Stasher.stub(:source).and_return("source")
|
9
|
+
LogStash::Time.stub(:now => 'timestamp')
|
10
|
+
end
|
11
|
+
|
12
|
+
subject(:subscriber) { Stasher::LogSubscriber.new }
|
13
|
+
|
14
|
+
describe '#start_processing' do
|
15
|
+
let(:payload) { FactoryGirl.create(:actioncontroller_payload) }
|
16
|
+
|
17
|
+
let(:event) {
|
18
|
+
ActiveSupport::Notifications::Event.new(
|
19
|
+
'process_action.action_controller', Time.now, Time.now, 2, payload
|
20
|
+
)
|
21
|
+
}
|
22
|
+
|
23
|
+
let(:json) {
|
24
|
+
'{"@source":"source","@tags":["request"],"@fields":{"method":"GET","ip":"127.0.0.1","params":{"foo":"bar"},' +
|
25
|
+
'"path":"/home","format":"application/json","controller":"home","action":"index"},"@timestamp":"timestamp"}' + "\n"
|
26
|
+
}
|
27
|
+
|
28
|
+
it 'calls all extractors and outputs the json' do
|
29
|
+
subscriber.should_receive(:extract_request).with(payload).and_return({:request => true})
|
30
|
+
subscriber.should_receive(:extract_current_scope).with(no_args).and_return({:custom => true})
|
31
|
+
subscriber.start_processing(event)
|
32
|
+
end
|
33
|
+
|
34
|
+
it "logs the event" do
|
35
|
+
subscriber.start_processing(event)
|
36
|
+
|
37
|
+
logger.messages.first.should == json
|
38
|
+
end
|
39
|
+
end
|
40
|
+
|
41
|
+
describe '#sql' do
|
42
|
+
let(:event) {
|
43
|
+
ActiveSupport::Notifications::Event.new(
|
44
|
+
'sql.active_record', Time.now, Time.now, 2, payload
|
45
|
+
)
|
46
|
+
}
|
47
|
+
|
48
|
+
context "for SCHEMA events" do
|
49
|
+
let(:payload) { FactoryGirl.create(:activerecord_sql_payload, name: 'SCHEMA') }
|
50
|
+
|
51
|
+
it "does not log anything" do
|
52
|
+
subscriber.sql(event)
|
53
|
+
|
54
|
+
logger.messages.should be_empty
|
55
|
+
end
|
56
|
+
end
|
57
|
+
|
58
|
+
context "for unnamed events" do
|
59
|
+
let(:payload) { FactoryGirl.create(:activerecord_sql_payload, name: '') }
|
60
|
+
|
61
|
+
it "does not log anything" do
|
62
|
+
subscriber.sql(event)
|
63
|
+
|
64
|
+
logger.messages.should be_empty
|
65
|
+
end
|
66
|
+
end
|
67
|
+
|
68
|
+
context "for session events" do
|
69
|
+
let(:payload) { FactoryGirl.create(:activerecord_sql_payload, name: 'ActiveRecord::SessionStore') }
|
70
|
+
|
71
|
+
it "does not log anything" do
|
72
|
+
subscriber.sql(event)
|
73
|
+
|
74
|
+
logger.messages.should be_empty
|
75
|
+
end
|
76
|
+
end
|
77
|
+
|
78
|
+
context "for any other events" do
|
79
|
+
let(:payload) { FactoryGirl.create(:activerecord_sql_payload) }
|
80
|
+
|
81
|
+
let(:json) {
|
82
|
+
'{"@source":"source","@tags":["sql"],"@fields":{"name":"User Load","sql":"' +
|
83
|
+
payload[:sql] + '","duration":0.0},"@timestamp":"timestamp"}' + "\n"
|
84
|
+
}
|
85
|
+
|
86
|
+
it 'calls all extractors and outputs the json' do
|
87
|
+
subscriber.should_receive(:extract_sql).with(payload).and_return({:sql => true})
|
88
|
+
subscriber.should_receive(:extract_current_scope).with(no_args).and_return({:custom => true})
|
89
|
+
subscriber.sql(event)
|
90
|
+
end
|
91
|
+
|
92
|
+
it "logs the event" do
|
93
|
+
subscriber.sql(event)
|
94
|
+
|
95
|
+
logger.messages.first.should == json
|
96
|
+
end
|
97
|
+
end
|
98
|
+
end
|
99
|
+
|
100
|
+
describe '#process_action' do
|
101
|
+
let(:payload) { FactoryGirl.create(:actioncontroller_payload) }
|
102
|
+
|
103
|
+
let(:event) {
|
104
|
+
ActiveSupport::Notifications::Event.new(
|
105
|
+
'process_action.action_controller', Time.now, Time.now, 2, payload
|
106
|
+
)
|
107
|
+
}
|
108
|
+
|
109
|
+
let(:json) {
|
110
|
+
'{"@source":"source","@tags":["response"],"@fields":{"method":"GET","ip":"127.0.0.1","params":{"foo":"bar"},' +
|
111
|
+
'"path":"/home","format":"application/json","controller":"home","action":"index","status":200,' +
|
112
|
+
'"duration":0.0,"view":0.01,"db":0.02},"@timestamp":"timestamp"}' + "\n"
|
113
|
+
}
|
114
|
+
|
115
|
+
it 'calls all extractors and outputs the json' do
|
116
|
+
subscriber.should_receive(:extract_request).with(payload).and_return({:request => true})
|
117
|
+
subscriber.should_receive(:extract_status).with(payload).and_return({:status => true})
|
118
|
+
subscriber.should_receive(:runtimes).with(event).and_return({:runtimes => true})
|
119
|
+
subscriber.should_receive(:extract_exception).with(payload).and_return({:exception => true})
|
120
|
+
subscriber.should_receive(:extract_current_scope).with(no_args).and_return({:custom => true})
|
121
|
+
subscriber.process_action(event)
|
122
|
+
end
|
123
|
+
|
124
|
+
it "logs the event" do
|
125
|
+
subscriber.process_action(event)
|
126
|
+
|
127
|
+
logger.messages.first.should == json
|
128
|
+
end
|
129
|
+
|
130
|
+
context "when the payload includes an exception" do
|
131
|
+
before :each do
|
132
|
+
payload[:exception] = [ 'Exception', 'message' ]
|
133
|
+
subscriber.stub(:extract_exception).and_return({})
|
134
|
+
end
|
135
|
+
|
136
|
+
it "adds the 'exception' tag" do
|
137
|
+
subscriber.process_action(event)
|
138
|
+
|
139
|
+
logger.messages.first.should match %r|"@tags":\["response","exception"\]|
|
140
|
+
end
|
141
|
+
end
|
142
|
+
|
143
|
+
it "clears the scoped parameters" do
|
144
|
+
Stasher::CurrentScope.should_receive(:clear!)
|
145
|
+
|
146
|
+
subscriber.process_action(event)
|
147
|
+
end
|
148
|
+
|
149
|
+
context "with a redirect" do
|
150
|
+
before do
|
151
|
+
Stasher::CurrentScope.fields[:location] = "http://www.example.com"
|
152
|
+
end
|
153
|
+
|
154
|
+
it "adds the location to the log line" do
|
155
|
+
subscriber.process_action(event)
|
156
|
+
logger.messages.first.should match %r|"@fields":{.*?"location":"http://www\.example\.com".*?}|
|
157
|
+
end
|
158
|
+
end
|
159
|
+
end
|
160
|
+
|
161
|
+
describe '#log_event' do
|
162
|
+
it "sets the type as a @tag" do
|
163
|
+
subscriber.send :log_event, 'tag', {}
|
164
|
+
|
165
|
+
logger.messages.first.should match %r|"@tags":\["tag"\]|
|
166
|
+
end
|
167
|
+
|
168
|
+
it "renders the data in the @fields" do
|
169
|
+
subscriber.send :log_event, 'tag', { "foo" => "bar", :baz => 'bot' }
|
170
|
+
|
171
|
+
logger.messages.first.should match %r|"@fields":{"foo":"bar","baz":"bot"}|
|
172
|
+
end
|
173
|
+
|
174
|
+
it "sets the @source" do
|
175
|
+
subscriber.send :log_event, 'tag', {}
|
176
|
+
|
177
|
+
logger.messages.first.should match %r|"@source":"source"|
|
178
|
+
end
|
179
|
+
|
180
|
+
context "with a block" do
|
181
|
+
it "calls the block with the new event" do
|
182
|
+
yielded = []
|
183
|
+
subscriber.send :log_event, 'tag', {} do |args|
|
184
|
+
yielded << args
|
185
|
+
end
|
186
|
+
|
187
|
+
yielded.size.should == 1
|
188
|
+
yielded.first.should be_a(LogStash::Event)
|
189
|
+
end
|
190
|
+
|
191
|
+
it "logs the modified event" do
|
192
|
+
subscriber.send :log_event, 'tag', {} do |event|
|
193
|
+
event.tags << "extra"
|
194
|
+
end
|
195
|
+
|
196
|
+
logger.messages.first.should match %r|"@tags":\["tag","extra"\]|
|
197
|
+
end
|
198
|
+
end
|
199
|
+
end
|
200
|
+
|
201
|
+
describe '#redirect_to' do
|
202
|
+
let(:event) {
|
203
|
+
ActiveSupport::Notifications::Event.new(
|
204
|
+
'redirect_to.action_controller', Time.now, Time.now, 1, :location => 'http://example.com', :status => 302
|
205
|
+
)
|
206
|
+
}
|
207
|
+
|
208
|
+
it "stores the payload location in the current scope" do
|
209
|
+
subscriber.redirect_to(event)
|
210
|
+
|
211
|
+
Stasher::CurrentScope.fields[:location].should == "http://example.com"
|
212
|
+
end
|
213
|
+
end
|
214
|
+
end
|
@@ -0,0 +1,90 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
|
3
|
+
describe Stasher::Logger do
|
4
|
+
subject (:logger) { Stasher::Logger.new }
|
5
|
+
|
6
|
+
before :each do
|
7
|
+
logger.level = ::Logger::WARN
|
8
|
+
end
|
9
|
+
|
10
|
+
it "logs messages that are at the configured level" do
|
11
|
+
Stasher.should_receive(:log).with('WARN', 'message')
|
12
|
+
|
13
|
+
logger.warn 'message'
|
14
|
+
end
|
15
|
+
|
16
|
+
it "logs messages that are above the configured level" do
|
17
|
+
Stasher.should_receive(:log).with('ERROR', 'message')
|
18
|
+
|
19
|
+
logger.error 'message'
|
20
|
+
end
|
21
|
+
|
22
|
+
it "does not log messages that are below the configured level" do
|
23
|
+
Stasher.should_not_receive(:log)
|
24
|
+
|
25
|
+
logger.info 'message'
|
26
|
+
end
|
27
|
+
|
28
|
+
it "formats the severity" do
|
29
|
+
Stasher.should_receive(:log).with('WARN', 'message')
|
30
|
+
|
31
|
+
logger.add ::Logger::WARN, "message"
|
32
|
+
end
|
33
|
+
|
34
|
+
it "returns true" do
|
35
|
+
Stasher.stub(:log)
|
36
|
+
|
37
|
+
logger.add( ::Logger::WARN, "message" ).should be_true
|
38
|
+
end
|
39
|
+
|
40
|
+
context "when there is a block given" do
|
41
|
+
it "yields to the block" do
|
42
|
+
Stasher.stub(:log)
|
43
|
+
|
44
|
+
expect { |b|
|
45
|
+
logger.add ::Logger::WARN, &b
|
46
|
+
}.to yield_with_no_args
|
47
|
+
end
|
48
|
+
|
49
|
+
it "logs the returned message" do
|
50
|
+
Stasher.should_receive(:log).with('WARN', 'message')
|
51
|
+
|
52
|
+
logger.add ::Logger::WARN do
|
53
|
+
"message"
|
54
|
+
end
|
55
|
+
end
|
56
|
+
end
|
57
|
+
|
58
|
+
context "when the message is a string" do
|
59
|
+
it "formats the message" do
|
60
|
+
logger.should_receive(:format_message).with(::Logger::WARN, an_instance_of(Time), nil, "message").and_return("formatted")
|
61
|
+
Stasher.stub(:log)
|
62
|
+
|
63
|
+
logger.warn 'message'
|
64
|
+
end
|
65
|
+
|
66
|
+
it "renders the formatted message" do
|
67
|
+
logger.stub(:format_message).and_return("formatted")
|
68
|
+
Stasher.should_receive(:log).with('WARN', 'formatted')
|
69
|
+
|
70
|
+
logger.warn 'message'
|
71
|
+
end
|
72
|
+
end
|
73
|
+
|
74
|
+
context "when the message is an object" do
|
75
|
+
let (:message) { Object.new }
|
76
|
+
|
77
|
+
it "does not format the message" do
|
78
|
+
logger.should_not_receive(:format_message)
|
79
|
+
Stasher.stub(:log)
|
80
|
+
|
81
|
+
logger.warn message
|
82
|
+
end
|
83
|
+
|
84
|
+
it "logs the raw message object" do
|
85
|
+
Stasher.should_receive(:log).with('WARN', message)
|
86
|
+
|
87
|
+
logger.warn message
|
88
|
+
end
|
89
|
+
end
|
90
|
+
end
|