request_log 0.0.1
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/.gitignore +3 -0
- data/Gemfile +4 -0
- data/Gemfile.lock +32 -0
- data/README.rdoc +83 -0
- data/Rakefile +8 -0
- data/lib/request_log.rb +5 -0
- data/lib/request_log/data.rb +37 -0
- data/lib/request_log/db.rb +41 -0
- data/lib/request_log/middleware.rb +40 -0
- data/lib/request_log/profiler.rb +106 -0
- data/lib/request_log/tasks.rb +2 -0
- data/lib/request_log/version.rb +3 -0
- data/lib/tasks/request_log.rake +33 -0
- data/request_log.gemspec +24 -0
- data/spec/data_spec.rb +28 -0
- data/spec/db_spec.rb +10 -0
- data/spec/middleware_spec.rb +65 -0
- data/spec/profiler_spec.rb +200 -0
- data/spec/spec_helper.rb +9 -0
- metadata +133 -0
data/.gitignore
ADDED
data/Gemfile
ADDED
data/Gemfile.lock
ADDED
@@ -0,0 +1,32 @@
|
|
1
|
+
PATH
|
2
|
+
remote: .
|
3
|
+
specs:
|
4
|
+
request_log (0.0.1)
|
5
|
+
|
6
|
+
GEM
|
7
|
+
remote: http://rubygems.org/
|
8
|
+
specs:
|
9
|
+
diff-lcs (1.1.2)
|
10
|
+
mocha (0.9.8)
|
11
|
+
rake
|
12
|
+
rack (1.2.1)
|
13
|
+
rake (0.8.7)
|
14
|
+
rspec (2.0.0)
|
15
|
+
rspec-core (= 2.0.0)
|
16
|
+
rspec-expectations (= 2.0.0)
|
17
|
+
rspec-mocks (= 2.0.0)
|
18
|
+
rspec-core (2.0.0)
|
19
|
+
rspec-expectations (2.0.0)
|
20
|
+
diff-lcs (>= 1.1.2)
|
21
|
+
rspec-mocks (2.0.0)
|
22
|
+
rspec-core (= 2.0.0)
|
23
|
+
rspec-expectations (= 2.0.0)
|
24
|
+
|
25
|
+
PLATFORMS
|
26
|
+
ruby
|
27
|
+
|
28
|
+
DEPENDENCIES
|
29
|
+
mocha (~> 0.9.8)
|
30
|
+
rack (~> 1.2.1)
|
31
|
+
request_log!
|
32
|
+
rspec (= 2.0.0)
|
data/README.rdoc
ADDED
@@ -0,0 +1,83 @@
|
|
1
|
+
= Request Log - Logging of web requests to MongoDB
|
2
|
+
|
3
|
+
Request Log is a Rack middleware for logging web requests to MongoDB. Each web request becomes a document in MongoDB
|
4
|
+
with fields like path, method, params, status, time, ip, runtime etc.
|
5
|
+
The gem offers support for monitoring the time overhead (usually very small) that the logging incurs.
|
6
|
+
The advantages of logging to MongoDB over logging to a plain text file are huge because of the query
|
7
|
+
capabilities of MongoDB. Here is an example of what a log document can look like:
|
8
|
+
|
9
|
+
summary: "GET / - 200 0.000303"
|
10
|
+
method: "GET"
|
11
|
+
path: "/"
|
12
|
+
ip: "10.218.1.177"
|
13
|
+
time: 2010-10-28 21:43:38 UTC
|
14
|
+
params: {"hello_world"=>"1"}
|
15
|
+
status: 200
|
16
|
+
runtime: 0.000303
|
17
|
+
|
18
|
+
You can easily customize which fields are stored in the log.
|
19
|
+
|
20
|
+
== Installation
|
21
|
+
|
22
|
+
Add gem dependencies with appropriate version numbers to your Gemfile (assuming you use Bundler):
|
23
|
+
|
24
|
+
gem 'mongo', '~> <latest-version-here>'
|
25
|
+
gem 'bson_ext', '~> <latest-version-here>'
|
26
|
+
gem 'request_log', :git => "http://github.com/peter/request_log.git"
|
27
|
+
|
28
|
+
Install with:
|
29
|
+
|
30
|
+
bundle install
|
31
|
+
|
32
|
+
Note that it's up to your application how it wants to connect to MongoDB (if at all) and the suggested
|
33
|
+
mongo and bson_ext gems are just suggestions.
|
34
|
+
|
35
|
+
Next you need to setup a MongoDB connection. Here is a MongoHQ example that in Rails would belong in config/initializers/request_log.rb:
|
36
|
+
|
37
|
+
if ENV['MONGOHQ_URL']
|
38
|
+
require 'uri'
|
39
|
+
require 'mongo'
|
40
|
+
uri = URI.parse(ENV['MONGOHQ_URL'])
|
41
|
+
connection = Mongo::Connection.from_uri(uri.to_s)
|
42
|
+
RequestLog::Db.mongo_db = connection.db(uri.path.gsub(/^\//, ''))
|
43
|
+
end
|
44
|
+
|
45
|
+
Now setup the Middleware in your config.ru file:
|
46
|
+
|
47
|
+
use RequestLog::Middleware
|
48
|
+
|
49
|
+
Here is an example of how you can customize the middleware:
|
50
|
+
|
51
|
+
use RequestLog::Middleware,
|
52
|
+
:logger => lambda { |data| ::RequestLog::Db.requests.insert(data.attributes.except(:summary)) },
|
53
|
+
:timeout => 0.5
|
54
|
+
|
55
|
+
In order to use the Rake tasks you need to make sure you have the MongoDB connection setup and that you
|
56
|
+
require the tasks in your Rakefile, like this:
|
57
|
+
|
58
|
+
require 'request_log'
|
59
|
+
require 'config/initializers/request_log.rb' # The file where you setup the mongo db connection
|
60
|
+
require 'request_log/tasks'
|
61
|
+
|
62
|
+
== Accessing the logs
|
63
|
+
|
64
|
+
You can tail the log like this:
|
65
|
+
|
66
|
+
rake request_log:tail
|
67
|
+
|
68
|
+
If you want to query the log and print a certain time period you can use request_log:print:
|
69
|
+
|
70
|
+
rake request_log:print from="2010-10-28 17:06:08" to="2010-10-28 17:06:10" conditions='status: 200'
|
71
|
+
|
72
|
+
If you are using MONGOHQ, remember to set the MONGOHQ_URL environment variable.
|
73
|
+
|
74
|
+
== Profiling
|
75
|
+
|
76
|
+
To monitor the time consumption and reliability of the MongoDB logging you can use the RequestLog::Profiler class.
|
77
|
+
It records number of failed and successful loggings, average and maximum logging times etc. To persist the profiling
|
78
|
+
information to MongoDB you can configure the profiler like this:
|
79
|
+
|
80
|
+
RequestLog::Profiler.persist_enabled = true
|
81
|
+
RequestLog::Profiler.persist_frequency = 1000 # persist profiling info every 1000 requests
|
82
|
+
|
83
|
+
The profiling info will then be written to a table (request_log_profiling) in the MongoDB database.
|
data/Rakefile
ADDED
data/lib/request_log.rb
ADDED
@@ -0,0 +1,37 @@
|
|
1
|
+
module RequestLog
|
2
|
+
class Data
|
3
|
+
attr_accessor :env, :status, :headers, :response, :app_time
|
4
|
+
|
5
|
+
def initialize(env, rack_response, app_time)
|
6
|
+
self.env = env
|
7
|
+
self.status = rack_response[0]
|
8
|
+
self.headers = rack_response[1]
|
9
|
+
self.response = rack_response[2]
|
10
|
+
self.app_time = app_time
|
11
|
+
end
|
12
|
+
|
13
|
+
def self.request_path(env)
|
14
|
+
env['PATH_INFO'] || env['REQUEST_PATH'] || "/"
|
15
|
+
end
|
16
|
+
|
17
|
+
def attributes
|
18
|
+
method = env['REQUEST_METHOD']
|
19
|
+
path = self.class.request_path(env)
|
20
|
+
{
|
21
|
+
:summary => "#{method} #{path} - #{status} #{app_time}",
|
22
|
+
:method => method,
|
23
|
+
:path => path,
|
24
|
+
:ip => env['REMOTE_ADDR'],
|
25
|
+
:time => Time.now.utc,
|
26
|
+
:params => params,
|
27
|
+
:status => status,
|
28
|
+
:runtime => app_time
|
29
|
+
}
|
30
|
+
end
|
31
|
+
|
32
|
+
def params
|
33
|
+
# NOTE: It seems getting POST params with ::Rack::Request.new(env).params does not work well with Rails
|
34
|
+
(env['action_controller.instance'] || ::Rack::Request.new(env)).params
|
35
|
+
end
|
36
|
+
end
|
37
|
+
end
|
@@ -0,0 +1,41 @@
|
|
1
|
+
module RequestLog
|
2
|
+
class Db
|
3
|
+
@@mongo_db = nil
|
4
|
+
|
5
|
+
def self.mongo_db=(mongo_db)
|
6
|
+
@@mongo_db = mongo_db
|
7
|
+
end
|
8
|
+
|
9
|
+
def self.mongo_db
|
10
|
+
@@mongo_db
|
11
|
+
end
|
12
|
+
|
13
|
+
def self.requests
|
14
|
+
mongo_db['requests']
|
15
|
+
end
|
16
|
+
|
17
|
+
def self.profiling
|
18
|
+
mongo_db['request_log_profiling']
|
19
|
+
end
|
20
|
+
|
21
|
+
def self.printable_request(request)
|
22
|
+
request.keys.reject { |key| key == "_id" }.map do |key|
|
23
|
+
"#{key}: #{request[key].inspect}"
|
24
|
+
end.join("\n")
|
25
|
+
end
|
26
|
+
|
27
|
+
def self.filtered_requests(start_time, end_time, conditions = {})
|
28
|
+
start_time = Time.parse(start_time).utc if start_time.is_a?(String)
|
29
|
+
end_time = Time.parse(end_time).utc if end_time.is_a?(String)
|
30
|
+
time_condition = {"time" => {"$gt" => start_time, "$lt" => end_time}}
|
31
|
+
requests.find(time_condition.merge(conditions))
|
32
|
+
end
|
33
|
+
|
34
|
+
def self.print_requests(start_time, end_time, conditions = {})
|
35
|
+
filtered_requests(start_time, end_time, conditions).each do |request|
|
36
|
+
puts printable_request(request)
|
37
|
+
puts
|
38
|
+
end
|
39
|
+
end
|
40
|
+
end
|
41
|
+
end
|
@@ -0,0 +1,40 @@
|
|
1
|
+
module RequestLog
|
2
|
+
class Middleware
|
3
|
+
def initialize(app, options = {})
|
4
|
+
@app = app
|
5
|
+
@logger = options[:logger] || lambda { |data| ::RequestLog::Db.requests.insert(data.attributes) }
|
6
|
+
@profiler = options[:profiler] || ::RequestLog::Profiler
|
7
|
+
@timeout = options[:timeout] || 0.3
|
8
|
+
@only_path = options[:only_path]
|
9
|
+
end
|
10
|
+
|
11
|
+
def call(env)
|
12
|
+
app_start = Time.now
|
13
|
+
rack_response = @app.call(env)
|
14
|
+
app_time = Time.now - app_start
|
15
|
+
return rack_response unless should_log?(env)
|
16
|
+
begin
|
17
|
+
logger_start = Time.now
|
18
|
+
Timeout::timeout(@timeout) do
|
19
|
+
@logger.call(::RequestLog::Data.new(env, rack_response, app_time))
|
20
|
+
end
|
21
|
+
@profiler.call(:result => :success, :elapsed_time => (Time.now - logger_start))
|
22
|
+
rescue Exception => e
|
23
|
+
@profiler.call(:result => :failure, :exception => e)
|
24
|
+
$stderr.puts("#{self.class}: exception #{e} #{e.backtrace.join("\n")}")
|
25
|
+
ensure
|
26
|
+
return rack_response
|
27
|
+
end
|
28
|
+
end
|
29
|
+
|
30
|
+
private
|
31
|
+
|
32
|
+
def should_log?(env)
|
33
|
+
if @only_path && ::RequestLog::Data.request_path(env) !~ @only_path
|
34
|
+
false
|
35
|
+
else
|
36
|
+
true
|
37
|
+
end
|
38
|
+
end
|
39
|
+
end
|
40
|
+
end
|
@@ -0,0 +1,106 @@
|
|
1
|
+
module RequestLog
|
2
|
+
class Profiler
|
3
|
+
def self.default_values
|
4
|
+
{
|
5
|
+
:success_count => 0,
|
6
|
+
:failure_count => 0,
|
7
|
+
:failure_exceptions => {},
|
8
|
+
:min_time => nil,
|
9
|
+
:max_time => nil,
|
10
|
+
:avg_time => nil,
|
11
|
+
:persist_enabled => false,
|
12
|
+
:persist_frequency => 2000
|
13
|
+
}
|
14
|
+
end
|
15
|
+
|
16
|
+
def self.attribute_names
|
17
|
+
default_values.keys
|
18
|
+
end
|
19
|
+
|
20
|
+
attribute_names.each do |attribute|
|
21
|
+
# cattr_accessor from ActiveSupport library
|
22
|
+
class_eval(<<-EOS, __FILE__, __LINE__ + 1)
|
23
|
+
@@#{attribute} = nil
|
24
|
+
|
25
|
+
def self.#{attribute}=(obj)
|
26
|
+
@@#{attribute} = obj
|
27
|
+
end
|
28
|
+
|
29
|
+
def self.#{attribute}
|
30
|
+
@@#{attribute}
|
31
|
+
end
|
32
|
+
EOS
|
33
|
+
end
|
34
|
+
|
35
|
+
def self.reset
|
36
|
+
attribute_names.each do |attribute|
|
37
|
+
send("#{attribute}=", default_values[attribute])
|
38
|
+
end
|
39
|
+
self
|
40
|
+
end
|
41
|
+
|
42
|
+
reset
|
43
|
+
|
44
|
+
def self.call(options = {})
|
45
|
+
if options[:result] == :success
|
46
|
+
self.success_count += 1
|
47
|
+
update_times(options[:elapsed_time])
|
48
|
+
else
|
49
|
+
self.failure_count += 1
|
50
|
+
if options[:exception]
|
51
|
+
failure_exceptions[options[:exception].class.name] ||= 0
|
52
|
+
failure_exceptions[options[:exception].class.name] += 1
|
53
|
+
end
|
54
|
+
end
|
55
|
+
persist! if should_persist?
|
56
|
+
end
|
57
|
+
|
58
|
+
def self.total_count
|
59
|
+
success_count + failure_count
|
60
|
+
end
|
61
|
+
|
62
|
+
def self.failure_ratio
|
63
|
+
failure_count.to_f/total_count
|
64
|
+
end
|
65
|
+
|
66
|
+
def self.attributes
|
67
|
+
attribute_names.inject({}) do |hash, attribute|
|
68
|
+
hash[attribute] = send(attribute)
|
69
|
+
hash
|
70
|
+
end
|
71
|
+
end
|
72
|
+
|
73
|
+
def self.persist!
|
74
|
+
::RequestLog::Db.profiling.insert(
|
75
|
+
:total_count => total_count,
|
76
|
+
:failure_ratio => failure_ratio,
|
77
|
+
:max_time => max_time,
|
78
|
+
:avg_time => avg_time,
|
79
|
+
:data => attributes
|
80
|
+
)
|
81
|
+
end
|
82
|
+
|
83
|
+
def self.should_persist?
|
84
|
+
persist_enabled && (total_count % persist_frequency == 0)
|
85
|
+
end
|
86
|
+
|
87
|
+
def self.to_s
|
88
|
+
attributes.inspect
|
89
|
+
end
|
90
|
+
|
91
|
+
private
|
92
|
+
|
93
|
+
def self.update_times(elapsed_time)
|
94
|
+
initialize_times(elapsed_time)
|
95
|
+
self.min_time = elapsed_time if elapsed_time < min_time
|
96
|
+
self.max_time = elapsed_time if elapsed_time > max_time
|
97
|
+
self.avg_time = (avg_time*(success_count - 1) + elapsed_time)/success_count
|
98
|
+
end
|
99
|
+
|
100
|
+
def self.initialize_times(elapsed_time)
|
101
|
+
self.min_time ||= elapsed_time
|
102
|
+
self.max_time ||= elapsed_time
|
103
|
+
self.avg_time ||= elapsed_time
|
104
|
+
end
|
105
|
+
end
|
106
|
+
end
|
@@ -0,0 +1,33 @@
|
|
1
|
+
require 'set'
|
2
|
+
|
3
|
+
namespace :request_log do
|
4
|
+
desc "Tail the request log"
|
5
|
+
task :tail do
|
6
|
+
wait_time = 10
|
7
|
+
printed_ids = Set.new
|
8
|
+
while(true)
|
9
|
+
RequestLog::Db.requests.find("time" => {"$gt" => (Time.now - wait_time).utc}).each do |r|
|
10
|
+
unless printed_ids.include?(r['_id'])
|
11
|
+
puts
|
12
|
+
puts RequestLog::Db.printable_request(r)
|
13
|
+
printed_ids << r['_id']
|
14
|
+
end
|
15
|
+
end
|
16
|
+
sleep (wait_time-1)
|
17
|
+
end
|
18
|
+
end
|
19
|
+
|
20
|
+
desc %q{Print a part of the log. Parameters: from='YYYY-MM-DD HH:MM' to='YYYY-MM-DD HH:MM' conditions=<ruby-hash-with-mongo-db-conditions>}
|
21
|
+
task :print do
|
22
|
+
from = ENV['from'] || (Time.now-600).utc
|
23
|
+
to = ENV['to'] || Time.now.utc
|
24
|
+
if conditions = ENV['conditions']
|
25
|
+
# We need to parse a Ruby hash here, let's not require braces
|
26
|
+
conditions = "{#{conditions}}" unless conditions[0] == "{"
|
27
|
+
conditions = eval(conditions)
|
28
|
+
else
|
29
|
+
conditions = {}
|
30
|
+
end
|
31
|
+
RequestLog::Db.print_requests(from, to, conditions)
|
32
|
+
end
|
33
|
+
end
|
data/request_log.gemspec
ADDED
@@ -0,0 +1,24 @@
|
|
1
|
+
# -*- encoding: utf-8 -*-
|
2
|
+
$:.push File.expand_path("../lib", __FILE__)
|
3
|
+
require "request_log/version"
|
4
|
+
|
5
|
+
Gem::Specification.new do |s|
|
6
|
+
s.name = "request_log"
|
7
|
+
s.version = RequestLog::VERSION
|
8
|
+
s.platform = Gem::Platform::RUBY
|
9
|
+
s.authors = ["Peter Marklund"]
|
10
|
+
s.email = ["peter@marklunds.com"]
|
11
|
+
s.homepage = ""
|
12
|
+
s.summary = %q{Rack middleware for logging web requests to a MongoDB database. Provides a profiler for monitoring logging overhead.}
|
13
|
+
|
14
|
+
s.rubyforge_project = "request_log"
|
15
|
+
|
16
|
+
s.files = `git ls-files`.split("\n")
|
17
|
+
s.test_files = `git ls-files -- {test,spec,features}/*`.split("\n")
|
18
|
+
s.executables = `git ls-files -- bin/*`.split("\n").map{ |f| File.basename(f) }
|
19
|
+
s.require_paths = ["lib"]
|
20
|
+
|
21
|
+
s.add_development_dependency "rspec", "2.0.0"
|
22
|
+
s.add_development_dependency "mocha", "~> 0.9.8"
|
23
|
+
s.add_development_dependency "rack", "~> 1.2.1"
|
24
|
+
end
|
data/spec/data_spec.rb
ADDED
@@ -0,0 +1,28 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
|
3
|
+
describe RequestLog::Data do
|
4
|
+
describe "attributes" do
|
5
|
+
before(:each) do
|
6
|
+
@env = env
|
7
|
+
@rack_response = [200, {"Content-Type" => "text/html"}, "Hello, World!"]
|
8
|
+
@app_time = 0.22666
|
9
|
+
@data = RequestLog::Data.new(@env, @rack_response, @app_time)
|
10
|
+
end
|
11
|
+
|
12
|
+
it "returns a hash with information about a request" do
|
13
|
+
puts "running spec"
|
14
|
+
attributes = @data.attributes
|
15
|
+
attributes[:summary].should =~ %r{GET /images/logo.png - 200 \d+\.\d+}
|
16
|
+
attributes[:runtime].to_s.should == attributes[:summary][/\d+\.\d+$/]
|
17
|
+
attributes[:time].should >= (Time.now-1).utc
|
18
|
+
attributes[:time].should <= Time.now.utc
|
19
|
+
attributes[:method].should == "GET"
|
20
|
+
attributes[:path].should == "/images/logo.png"
|
21
|
+
attributes[:ip].should == "127.0.0.1"
|
22
|
+
end
|
23
|
+
end
|
24
|
+
|
25
|
+
def env
|
26
|
+
{"GATEWAY_INTERFACE"=>"CGI/1.1", "PATH_INFO"=>"/images/logo.png", "QUERY_STRING"=>"", "REMOTE_ADDR"=>"127.0.0.1", "REMOTE_HOST"=>"www.publish.newsdesk.local", "REQUEST_METHOD"=>"GET", "REQUEST_URI"=>"http://publish.lvh.me:3000/images/logo.png", "SCRIPT_NAME"=>"", "SERVER_NAME"=>"publish.lvh.me", "SERVER_PORT"=>"3000", "SERVER_PROTOCOL"=>"HTTP/1.1", "SERVER_SOFTWARE"=>"WEBrick/1.3.1 (Ruby/1.9.2/2010-08-18)", "HTTP_HOST"=>"publish.lvh.me:3000", "HTTP_USER_AGENT"=>"Mozilla/5.0 (Macintosh; U; Intel Mac OS X 10.6; en-US; rv:1.9.2.2) Gecko/20100316 Firefox/3.6.2", "HTTP_ACCEPT"=>"image/png,image/*;q=0.8,*/*;q=0.5", "HTTP_ACCEPT_LANGUAGE"=>"en-us,en;q=0.5", "HTTP_ACCEPT_ENCODING"=>"gzip,deflate", "HTTP_ACCEPT_CHARSET"=>"ISO-8859-1,utf-8;q=0.7,*;q=0.7", "HTTP_KEEP_ALIVE"=>"115", "HTTP_CONNECTION"=>"keep-alive", "HTTP_REFERER"=>"http://publish.lvh.me:3000/system/profiling", "HTTP_AUTHORIZATION"=>"Basic YWRtaW46aWx3d3NwYTIwMTA=", "rack.version"=>[1, 1], "rack.input"=>StringIO.new, "rack.errors"=>$stderr, "rack.multithread"=>true, "rack.multiprocess"=>false, "rack.run_once"=>false, "rack.url_scheme"=>"http", "HTTP_VERSION"=>"HTTP/1.1", "REQUEST_PATH"=>"/"}
|
27
|
+
end
|
28
|
+
end
|
data/spec/db_spec.rb
ADDED
@@ -0,0 +1,65 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
|
3
|
+
describe RequestLog::Middleware do
|
4
|
+
describe "call" do
|
5
|
+
it "invokes the logger with the data and the profiler in case of success" do
|
6
|
+
@env = {"REQUEST_PATH" => "/v1/foobar", "HTTP_USER_AGENT" => "Mozilla/5.0"}
|
7
|
+
@logger = mock('logger')
|
8
|
+
@logger.expects(:call).with() { |data| data.is_a?(RequestLog::Data) && data.status == 200 && data.env == @env }
|
9
|
+
@profiler = mock('profiler')
|
10
|
+
@profiler.expects(:call).with() { |options| options[:result] == :success && options[:elapsed_time] > 0 }
|
11
|
+
@rack_response = [200, {"Content-Type" => "text/html"}, "Hello, World!"]
|
12
|
+
@app = lambda { |env| @rack_response }
|
13
|
+
@middleware = RequestLog::Middleware.new(@app, :logger => @logger, :profiler => @profiler, :only_path => %r{\A/v\d})
|
14
|
+
@middleware.call(@env).should == @rack_response
|
15
|
+
end
|
16
|
+
|
17
|
+
it "uses RequestLog::Db.requests.insert as a default logger if no logger is specified" do
|
18
|
+
@env = env.merge("REQUEST_PATH" => "/v1/foobar", "HTTP_USER_AGENT" => "Mozilla/5.0")
|
19
|
+
requests = mock('requests')
|
20
|
+
requests.expects(:insert).with() { |attributes| attributes.is_a?(Hash) && attributes[:status] == 200 && attributes[:path] == '/v1/foobar' }
|
21
|
+
::RequestLog::Db.stubs(:requests).returns(requests)
|
22
|
+
@rack_response = [200, {"Content-Type" => "text/html"}, "Hello, World!"]
|
23
|
+
@app = lambda { |env| @rack_response }
|
24
|
+
@middleware = RequestLog::Middleware.new(@app)
|
25
|
+
@middleware.call(@env).should == @rack_response
|
26
|
+
end
|
27
|
+
|
28
|
+
it "never calls logger if response content type doesn't match only_path option" do
|
29
|
+
@env = {"REQUEST_PATH" => "/foobar", "HTTP_USER_AGENT" => "Mozilla/5.0"}
|
30
|
+
@logger = mock('logger')
|
31
|
+
@logger.expects(:call).never
|
32
|
+
@profiler = mock('profiler')
|
33
|
+
@profiler.expects(:call).never
|
34
|
+
@rack_response = [200, {"Content-Type" => "text/html"}, "Hello, World!"]
|
35
|
+
@app = lambda { |env| @rack_response }
|
36
|
+
@middleware = RequestLog::Middleware.new(@app, :logger => @logger, :profiler => @profiler, :only_path => %r{\A/v\d})
|
37
|
+
@middleware.call(@env).should == @rack_response
|
38
|
+
end
|
39
|
+
|
40
|
+
it "invokes the logger with the data and the profiler if RuntimeError is raised" do
|
41
|
+
call_middleware_with_exception(RuntimeError)
|
42
|
+
end
|
43
|
+
|
44
|
+
it "invokes the logger with the data and the profiler if Timeout::Error is raised" do
|
45
|
+
call_middleware_with_exception(Timeout::Error)
|
46
|
+
end
|
47
|
+
|
48
|
+
def call_middleware_with_exception(exception_class)
|
49
|
+
@env = {"REQUEST_PATH" => "/", "HTTP_USER_AGENT" => "Mozilla/5.0"}
|
50
|
+
@logger = mock('logger')
|
51
|
+
@logger.expects(:call).raises(exception_class)
|
52
|
+
@profiler = mock('profiler')
|
53
|
+
@profiler.expects(:call).with() { |options| options[:result] == :failure && options[:exception].is_a?(exception_class) }
|
54
|
+
@rack_response = [200, {"Content-Type" => "text/html"}, "Hello, World!"]
|
55
|
+
@app = lambda { |env| @rack_response }
|
56
|
+
$stderr.expects(:puts).with() { |error_string| error_string =~ /#{exception_class.name}/ }
|
57
|
+
@middleware = RequestLog::Middleware.new(@app, :logger => @logger, :profiler => @profiler)
|
58
|
+
@middleware.call(@env).should == @rack_response
|
59
|
+
end
|
60
|
+
end
|
61
|
+
|
62
|
+
def env
|
63
|
+
{"GATEWAY_INTERFACE"=>"CGI/1.1", "QUERY_STRING"=>"", "REMOTE_ADDR"=>"127.0.0.1", "REMOTE_HOST"=>"www.publish.newsdesk.local", "REQUEST_METHOD"=>"GET", "REQUEST_URI"=>"http://publish.lvh.me:3000/images/logo.png", "SCRIPT_NAME"=>"", "SERVER_NAME"=>"publish.lvh.me", "SERVER_PORT"=>"3000", "SERVER_PROTOCOL"=>"HTTP/1.1", "SERVER_SOFTWARE"=>"WEBrick/1.3.1 (Ruby/1.9.2/2010-08-18)", "HTTP_HOST"=>"publish.lvh.me:3000", "HTTP_USER_AGENT"=>"Mozilla/5.0 (Macintosh; U; Intel Mac OS X 10.6; en-US; rv:1.9.2.2) Gecko/20100316 Firefox/3.6.2", "HTTP_ACCEPT"=>"image/png,image/*;q=0.8,*/*;q=0.5", "HTTP_ACCEPT_LANGUAGE"=>"en-us,en;q=0.5", "HTTP_ACCEPT_ENCODING"=>"gzip,deflate", "HTTP_ACCEPT_CHARSET"=>"ISO-8859-1,utf-8;q=0.7,*;q=0.7", "HTTP_KEEP_ALIVE"=>"115", "HTTP_CONNECTION"=>"keep-alive", "HTTP_REFERER"=>"http://publish.lvh.me:3000/system/profiling", "HTTP_AUTHORIZATION"=>"Basic YWRtaW46aWx3d3NwYTIwMTA=", "rack.version"=>[1, 1], "rack.input"=>StringIO.new, "rack.errors"=>$stderr, "rack.multithread"=>true, "rack.multiprocess"=>false, "rack.run_once"=>false, "rack.url_scheme"=>"http", "HTTP_VERSION"=>"HTTP/1.1", "REQUEST_PATH"=>"/"}
|
64
|
+
end
|
65
|
+
end
|
@@ -0,0 +1,200 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
|
3
|
+
describe RequestLog::Profiler do
|
4
|
+
before(:each) do
|
5
|
+
profiler.reset
|
6
|
+
end
|
7
|
+
|
8
|
+
describe "call" do
|
9
|
+
describe "result => success" do
|
10
|
+
it "increments success_count and time stats" do
|
11
|
+
profiler.success_count.should == 0
|
12
|
+
profiler.call(:result => :success, :elapsed_time => 0.5)
|
13
|
+
profiler.success_count.should == 1
|
14
|
+
profiler.total_count.should == 1
|
15
|
+
profiler.min_time.should == 0.5
|
16
|
+
profiler.max_time.should == 0.5
|
17
|
+
profiler.avg_time.should == 0.5
|
18
|
+
profiler.call(:result => :success, :elapsed_time => 0.6)
|
19
|
+
profiler.success_count.should == 2
|
20
|
+
profiler.total_count.should == 2
|
21
|
+
profiler.min_time.should == 0.5
|
22
|
+
profiler.max_time.should == 0.6
|
23
|
+
profiler.avg_time.should == 0.55
|
24
|
+
profiler.call(:result => :success, :elapsed_time => 0.7)
|
25
|
+
profiler.success_count.should == 3
|
26
|
+
profiler.min_time.should == 0.5
|
27
|
+
profiler.max_time.should == 0.7
|
28
|
+
profiler.avg_time.should == 0.6
|
29
|
+
end
|
30
|
+
end
|
31
|
+
|
32
|
+
describe "result => failure" do
|
33
|
+
it "increments failure_count" do
|
34
|
+
profiler.failure_count.should == 0
|
35
|
+
profiler.call(:result => :failure)
|
36
|
+
profiler.failure_count.should == 1
|
37
|
+
profiler.total_count.should == 1
|
38
|
+
profiler.call(:result => :failure)
|
39
|
+
profiler.failure_count.should == 2
|
40
|
+
profiler.total_count.should == 2
|
41
|
+
end
|
42
|
+
|
43
|
+
it "sets failure_exceptions if an exception is passed" do
|
44
|
+
profiler.failure_count.should == 0
|
45
|
+
profiler.failure_exceptions.should == {}
|
46
|
+
profiler.call(:result => :failure, :exception => RuntimeError.new("some exception"))
|
47
|
+
profiler.failure_count.should == 1
|
48
|
+
profiler.failure_exceptions.should == {"RuntimeError" => 1}
|
49
|
+
profiler.call(:result => :failure, :exception => Timeout::Error.new("some timeout"))
|
50
|
+
profiler.failure_count.should == 2
|
51
|
+
profiler.failure_exceptions.should == {"RuntimeError" => 1, "Timeout::Error" => 1}
|
52
|
+
profiler.call(:result => :failure, :exception => Timeout::Error.new("some timeout"))
|
53
|
+
profiler.failure_count.should == 3
|
54
|
+
profiler.failure_exceptions.should == {"RuntimeError" => 1, "Timeout::Error" => 2}
|
55
|
+
end
|
56
|
+
end
|
57
|
+
|
58
|
+
describe "persist!" do
|
59
|
+
it "does not get invoked if should_persist? == false" do
|
60
|
+
profiler.stubs(:should_persist?).returns(false)
|
61
|
+
profiler.expects(:persist!).never
|
62
|
+
profiler.call(:result => :success, :elapsed_time => 0.6)
|
63
|
+
end
|
64
|
+
|
65
|
+
it "does get invoked if should_persist? == true" do
|
66
|
+
profiler.stubs(:should_persist?).returns(true)
|
67
|
+
profiler.expects(:persist!).once
|
68
|
+
profiler.call(:result => :success, :elapsed_time => 0.6)
|
69
|
+
end
|
70
|
+
end
|
71
|
+
end
|
72
|
+
|
73
|
+
describe "total_count" do
|
74
|
+
it "is failure_count + success_count" do
|
75
|
+
profiler.failure_count = 427
|
76
|
+
profiler.success_count = 10000
|
77
|
+
profiler.total_count.should == 10427
|
78
|
+
end
|
79
|
+
end
|
80
|
+
|
81
|
+
describe "failure_ratio" do
|
82
|
+
it "is failure_count/total_count" do
|
83
|
+
profiler.failure_count = 1
|
84
|
+
profiler.success_count = 0
|
85
|
+
profiler.failure_ratio.should == 1.0
|
86
|
+
|
87
|
+
profiler.failure_count = 1
|
88
|
+
profiler.success_count = 1
|
89
|
+
profiler.failure_ratio.should == 0.5
|
90
|
+
|
91
|
+
profiler.failure_count = 1
|
92
|
+
profiler.success_count = 9
|
93
|
+
profiler.failure_ratio.should == 0.1
|
94
|
+
end
|
95
|
+
end
|
96
|
+
|
97
|
+
describe "persist!" do
|
98
|
+
it "stores profiling info in mongo db" do
|
99
|
+
profiler.success_count = 80
|
100
|
+
profiler.failure_count = 20
|
101
|
+
profiler.avg_time = 0.0003
|
102
|
+
profiler.max_time = 0.09
|
103
|
+
profiling = mock('profiling')
|
104
|
+
profiling.expects(:insert).with(
|
105
|
+
:total_count => profiler.total_count,
|
106
|
+
:failure_ratio => profiler.failure_ratio,
|
107
|
+
:max_time => profiler.max_time,
|
108
|
+
:avg_time => profiler.avg_time,
|
109
|
+
:data => profiler.attributes
|
110
|
+
)
|
111
|
+
RequestLog::Db.expects(:profiling).returns(profiling)
|
112
|
+
profiler.persist!
|
113
|
+
end
|
114
|
+
end
|
115
|
+
|
116
|
+
describe "persist_enabled" do
|
117
|
+
it "defaults to false" do
|
118
|
+
profiler.reset.persist_enabled.should == false
|
119
|
+
end
|
120
|
+
|
121
|
+
it "can be set to true" do
|
122
|
+
profiler.persist_enabled = true
|
123
|
+
profiler.persist_enabled.should == true
|
124
|
+
end
|
125
|
+
end
|
126
|
+
|
127
|
+
describe "should_persist?" do
|
128
|
+
before(:each) do
|
129
|
+
profiler.persist_enabled = true
|
130
|
+
end
|
131
|
+
|
132
|
+
it "returns true if total_count % persist_frequency == 0" do
|
133
|
+
profiler.persist_frequency = 10
|
134
|
+
profiler.success_count = 5
|
135
|
+
profiler.failure_count = 5
|
136
|
+
profiler.should_persist?.should be_true
|
137
|
+
|
138
|
+
profiler.persist_frequency = 3
|
139
|
+
profiler.success_count = 80
|
140
|
+
profiler.failure_count = 10
|
141
|
+
profiler.should_persist?.should be_true
|
142
|
+
end
|
143
|
+
|
144
|
+
it "returns false if total_count % persist_frequency != 0" do
|
145
|
+
profiler.persist_frequency = 3
|
146
|
+
profiler.success_count = 5
|
147
|
+
profiler.failure_count = 5
|
148
|
+
profiler.should_persist?.should be_false
|
149
|
+
end
|
150
|
+
|
151
|
+
it "returns false if persist_enabled = false" do
|
152
|
+
profiler.persist_frequency = 10
|
153
|
+
profiler.success_count = 5
|
154
|
+
profiler.failure_count = 5
|
155
|
+
profiler.persist_enabled = false
|
156
|
+
profiler.should_persist?.should be_false
|
157
|
+
end
|
158
|
+
end
|
159
|
+
|
160
|
+
describe "attribute_names" do
|
161
|
+
it "has accessors" do
|
162
|
+
profiler.attribute_names.each do |attribute|
|
163
|
+
profiler.send(attribute).should == profiler.default_values[attribute]
|
164
|
+
profiler.send("#{attribute}=", "foobar")
|
165
|
+
profiler.send(attribute).should == "foobar"
|
166
|
+
end
|
167
|
+
end
|
168
|
+
end
|
169
|
+
|
170
|
+
describe "attributes" do
|
171
|
+
it "returns a hash with the attribute values" do
|
172
|
+
profiler.min_time = 3.0
|
173
|
+
profiler.attributes.should == profiler.default_values.merge(:min_time => 3.0)
|
174
|
+
end
|
175
|
+
end
|
176
|
+
|
177
|
+
describe "to_s" do
|
178
|
+
it "returns the inspect of the attributes" do
|
179
|
+
profiler.to_s.should == profiler.attributes.inspect
|
180
|
+
end
|
181
|
+
end
|
182
|
+
|
183
|
+
describe "reset" do
|
184
|
+
it "resets all attributes to default values" do
|
185
|
+
profiler.attribute_names.each do |attribute|
|
186
|
+
profiler.send("#{attribute}=", "foobar")
|
187
|
+
end
|
188
|
+
profiler.reset
|
189
|
+
profiler.attribute_names.each do |attribute|
|
190
|
+
profiler.send(attribute).should == profiler.default_values[attribute]
|
191
|
+
end
|
192
|
+
end
|
193
|
+
end
|
194
|
+
|
195
|
+
|
196
|
+
|
197
|
+
def profiler
|
198
|
+
RequestLog::Profiler
|
199
|
+
end
|
200
|
+
end
|
data/spec/spec_helper.rb
ADDED
metadata
ADDED
@@ -0,0 +1,133 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: request_log
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
prerelease: false
|
5
|
+
segments:
|
6
|
+
- 0
|
7
|
+
- 0
|
8
|
+
- 1
|
9
|
+
version: 0.0.1
|
10
|
+
platform: ruby
|
11
|
+
authors:
|
12
|
+
- Peter Marklund
|
13
|
+
autorequire:
|
14
|
+
bindir: bin
|
15
|
+
cert_chain: []
|
16
|
+
|
17
|
+
date: 2010-10-29 00:00:00 +02:00
|
18
|
+
default_executable:
|
19
|
+
dependencies:
|
20
|
+
- !ruby/object:Gem::Dependency
|
21
|
+
name: rspec
|
22
|
+
requirement: &id001 !ruby/object:Gem::Requirement
|
23
|
+
none: false
|
24
|
+
requirements:
|
25
|
+
- - "="
|
26
|
+
- !ruby/object:Gem::Version
|
27
|
+
segments:
|
28
|
+
- 2
|
29
|
+
- 0
|
30
|
+
- 0
|
31
|
+
version: 2.0.0
|
32
|
+
type: :development
|
33
|
+
prerelease: false
|
34
|
+
version_requirements: *id001
|
35
|
+
- !ruby/object:Gem::Dependency
|
36
|
+
name: mocha
|
37
|
+
requirement: &id002 !ruby/object:Gem::Requirement
|
38
|
+
none: false
|
39
|
+
requirements:
|
40
|
+
- - ~>
|
41
|
+
- !ruby/object:Gem::Version
|
42
|
+
segments:
|
43
|
+
- 0
|
44
|
+
- 9
|
45
|
+
- 8
|
46
|
+
version: 0.9.8
|
47
|
+
type: :development
|
48
|
+
prerelease: false
|
49
|
+
version_requirements: *id002
|
50
|
+
- !ruby/object:Gem::Dependency
|
51
|
+
name: rack
|
52
|
+
requirement: &id003 !ruby/object:Gem::Requirement
|
53
|
+
none: false
|
54
|
+
requirements:
|
55
|
+
- - ~>
|
56
|
+
- !ruby/object:Gem::Version
|
57
|
+
segments:
|
58
|
+
- 1
|
59
|
+
- 2
|
60
|
+
- 1
|
61
|
+
version: 1.2.1
|
62
|
+
type: :development
|
63
|
+
prerelease: false
|
64
|
+
version_requirements: *id003
|
65
|
+
description:
|
66
|
+
email:
|
67
|
+
- peter@marklunds.com
|
68
|
+
executables: []
|
69
|
+
|
70
|
+
extensions: []
|
71
|
+
|
72
|
+
extra_rdoc_files: []
|
73
|
+
|
74
|
+
files:
|
75
|
+
- .gitignore
|
76
|
+
- Gemfile
|
77
|
+
- Gemfile.lock
|
78
|
+
- README.rdoc
|
79
|
+
- Rakefile
|
80
|
+
- lib/request_log.rb
|
81
|
+
- lib/request_log/data.rb
|
82
|
+
- lib/request_log/db.rb
|
83
|
+
- lib/request_log/middleware.rb
|
84
|
+
- lib/request_log/profiler.rb
|
85
|
+
- lib/request_log/tasks.rb
|
86
|
+
- lib/request_log/version.rb
|
87
|
+
- lib/tasks/request_log.rake
|
88
|
+
- request_log.gemspec
|
89
|
+
- spec/data_spec.rb
|
90
|
+
- spec/db_spec.rb
|
91
|
+
- spec/middleware_spec.rb
|
92
|
+
- spec/profiler_spec.rb
|
93
|
+
- spec/spec_helper.rb
|
94
|
+
has_rdoc: true
|
95
|
+
homepage: ""
|
96
|
+
licenses: []
|
97
|
+
|
98
|
+
post_install_message:
|
99
|
+
rdoc_options: []
|
100
|
+
|
101
|
+
require_paths:
|
102
|
+
- lib
|
103
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
104
|
+
none: false
|
105
|
+
requirements:
|
106
|
+
- - ">="
|
107
|
+
- !ruby/object:Gem::Version
|
108
|
+
hash: -1014350961
|
109
|
+
segments:
|
110
|
+
- 0
|
111
|
+
version: "0"
|
112
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
113
|
+
none: false
|
114
|
+
requirements:
|
115
|
+
- - ">="
|
116
|
+
- !ruby/object:Gem::Version
|
117
|
+
hash: -1014350961
|
118
|
+
segments:
|
119
|
+
- 0
|
120
|
+
version: "0"
|
121
|
+
requirements: []
|
122
|
+
|
123
|
+
rubyforge_project: request_log
|
124
|
+
rubygems_version: 1.3.7
|
125
|
+
signing_key:
|
126
|
+
specification_version: 3
|
127
|
+
summary: Rack middleware for logging web requests to a MongoDB database. Provides a profiler for monitoring logging overhead.
|
128
|
+
test_files:
|
129
|
+
- spec/data_spec.rb
|
130
|
+
- spec/db_spec.rb
|
131
|
+
- spec/middleware_spec.rb
|
132
|
+
- spec/profiler_spec.rb
|
133
|
+
- spec/spec_helper.rb
|