samuel 0.2.1 → 0.3.0
Sign up to get free protection for your applications and to get access to all the features.
- data/LICENSE +1 -1
- data/README.rdoc +33 -19
- data/Rakefile +7 -10
- data/lib/samuel.rb +13 -13
- data/lib/samuel/diary.rb +29 -0
- data/lib/samuel/driver_patches/http_client.rb +54 -0
- data/lib/samuel/driver_patches/net_http.rb +42 -0
- data/lib/samuel/loader.rb +19 -0
- data/lib/samuel/log_entries/base.rb +76 -0
- data/lib/samuel/log_entries/http_client.rb +28 -0
- data/lib/samuel/log_entries/net_http.rb +45 -0
- data/samuel.gemspec +25 -16
- data/test/http_client_test.rb +97 -0
- data/test/loader_test.rb +63 -0
- data/test/{request_test.rb → net_http_test.rb} +29 -2
- data/test/test_helper.rb +36 -5
- metadata +19 -11
- data/VERSION +0 -1
- data/lib/samuel/net_http.rb +0 -10
- data/lib/samuel/request.rb +0 -96
data/LICENSE
CHANGED
data/README.rdoc
CHANGED
@@ -1,13 +1,16 @@
|
|
1
1
|
= Samuel
|
2
2
|
|
3
|
-
Samuel is a gem for automatic logging of your
|
3
|
+
Samuel is a gem for automatic logging of your HTTP requests. It's named for
|
4
4
|
the serial diarist Mr. Pepys, who was known to reliably record events both
|
5
5
|
quotidian and remarkable.
|
6
6
|
|
7
7
|
Should a Great Plague, Fire, or Whale befall an important external web service
|
8
8
|
you use, you'll be sure to have a tidy record of it.
|
9
9
|
|
10
|
-
|
10
|
+
It supports both Net::HTTP and HTTPClient (formerly HTTPAccess2),
|
11
|
+
automatically loading the correct logger for the HTTP client you're using.
|
12
|
+
|
13
|
+
== Usage
|
11
14
|
|
12
15
|
When Rails is loaded, Samuel configures a few things automatically. So all you
|
13
16
|
need to do is this:
|
@@ -24,6 +27,14 @@ For non-Rails projects, you'll have to manually configure logging, like this:
|
|
24
27
|
|
25
28
|
If you don't assign a logger, Samuel will configure a default logger on +STDOUT+.
|
26
29
|
|
30
|
+
== HTTP Clients
|
31
|
+
|
32
|
+
When you load Samuel, it automatically detects which HTTP clients you've
|
33
|
+
loaded, then patches them to add logging. If no HTTP drivers are loaded when
|
34
|
+
you load Samuel, it will automatically load Net::HTTP for you. (So, if you're
|
35
|
+
using HTTPClient or a library based on it, make sure to require it before you
|
36
|
+
require Samuel.)
|
37
|
+
|
27
38
|
== Configuration
|
28
39
|
|
29
40
|
There are two ways to specify configuration options for Samuel: global and
|
@@ -44,27 +55,30 @@ configuration for a set of HTTP requests:
|
|
44
55
|
|
45
56
|
Right now, there are three configuration changes you can make in either style:
|
46
57
|
|
47
|
-
*
|
48
|
-
values. If a request domain includes one of the domain substrings,
|
49
|
-
corresponding label will be used for the first part of that log entry.
|
50
|
-
default this is set to <tt
|
58
|
+
* <tt>:labels</tt> -- This is a hash with domain substrings as keys and log
|
59
|
+
labels as values. If a request domain includes one of the domain substrings,
|
60
|
+
the corresponding label will be used for the first part of that log entry.
|
61
|
+
By default this is set to <tt>{"" => "HTTP"}</tt>, so that all requests are
|
51
62
|
labeled with <tt>"HTTP Request"</tt>.
|
52
|
-
|
53
|
-
|
54
|
-
|
55
|
-
|
56
|
-
|
57
|
-
|
58
|
-
|
59
|
-
|
60
|
-
|
61
|
-
|
62
|
-
|
63
|
-
|
63
|
+
|
64
|
+
* <tt>:label</tt> -- As an alternative to the <tt>:labels</tt> hash, this is
|
65
|
+
simply a string. If set, it takes precedence over any <tt>:labels</tt> (by
|
66
|
+
default, it's not set). It gets <tt>"Request"</tt> appended to it as well --
|
67
|
+
so if you want your log to always say <tt>Twitter API Request</tt> instead
|
68
|
+
of the default <tt>HTTP Request</tt>, you can set this to <tt>"Twitter
|
69
|
+
API"</tt>. I'd recommend using this setting globally if you're only making
|
70
|
+
requests to one service, or inline if you just need to temporarily override
|
71
|
+
the global <tt>:labels</tt>.
|
72
|
+
|
73
|
+
* <tt>:filtered_params</tt> -- This works just like Rails's
|
74
|
+
+filter_parameter_logging+ method. Set it to a symbol, string, or array of
|
75
|
+
them, and Samuel will filter the value of query parameters that have any of
|
76
|
+
these patterns as a substring by replacing the value with
|
77
|
+
<tt>[FILTERED]</tt> in your logs. By default, no filtering is enabled.
|
64
78
|
|
65
79
|
Samuel logs successful HTTP requests at the +INFO+ level; Failed requests log at
|
66
80
|
the +WARN+ level. This isn't currently configurable, but it's on the list.
|
67
81
|
|
68
82
|
== License
|
69
83
|
|
70
|
-
Copyright 2009 Chris Kampmeier. See +LICENSE+ for details.
|
84
|
+
Copyright 2009–2010 Chris Kampmeier. See +LICENSE+ for details.
|
data/Rakefile
CHANGED
@@ -5,23 +5,20 @@ begin
|
|
5
5
|
require 'jeweler'
|
6
6
|
Jeweler::Tasks.new do |gem|
|
7
7
|
gem.name = "samuel"
|
8
|
+
gem.version = "0.3.0"
|
8
9
|
gem.summary = %Q{An automatic logger for HTTP requests in Ruby}
|
9
|
-
gem.description = %Q{An automatic logger for HTTP requests in Ruby
|
10
|
+
gem.description = %Q{An automatic logger for HTTP requests in Ruby, supporting the Net::HTTP and HTTPClient client libraries.}
|
10
11
|
gem.email = "chris@kampers.net"
|
11
12
|
gem.homepage = "http://github.com/chrisk/samuel"
|
12
13
|
gem.authors = ["Chris Kampmeier"]
|
13
14
|
gem.rubyforge_project = "samuel"
|
14
|
-
gem.add_development_dependency "
|
15
|
-
gem.add_development_dependency "yard"
|
15
|
+
gem.add_development_dependency "shoulda"
|
16
16
|
gem.add_development_dependency "mocha"
|
17
|
+
gem.add_development_dependency "httpclient"
|
17
18
|
gem.add_development_dependency "fakeweb"
|
18
19
|
end
|
19
|
-
Jeweler::GemcutterTasks.new
|
20
|
-
Jeweler::RubyforgeTasks.new do |rubyforge|
|
21
|
-
rubyforge.doc_task = "yardoc"
|
22
|
-
end
|
23
20
|
rescue LoadError
|
24
|
-
puts "Jeweler (or a dependency) not available. Install it with:
|
21
|
+
puts "Jeweler (or a dependency) not available. Install it with: gem install jeweler"
|
25
22
|
end
|
26
23
|
|
27
24
|
require 'rake/testtask'
|
@@ -44,7 +41,7 @@ begin
|
|
44
41
|
end
|
45
42
|
rescue LoadError
|
46
43
|
task :rcov do
|
47
|
-
abort "RCov is not available. In order to run rcov, you must:
|
44
|
+
abort "RCov is not available. In order to run rcov, you must: gem install rcov"
|
48
45
|
end
|
49
46
|
end
|
50
47
|
|
@@ -57,6 +54,6 @@ begin
|
|
57
54
|
YARD::Rake::YardocTask.new
|
58
55
|
rescue LoadError
|
59
56
|
task :yardoc do
|
60
|
-
abort "YARD is not available. In order to run yardoc, you must:
|
57
|
+
abort "YARD is not available. In order to run yardoc, you must: gem install yard"
|
61
58
|
end
|
62
59
|
end
|
data/lib/samuel.rb
CHANGED
@@ -1,16 +1,21 @@
|
|
1
1
|
require "logger"
|
2
|
-
require "
|
3
|
-
require "net/https"
|
4
|
-
require "benchmark"
|
2
|
+
require "forwardable"
|
5
3
|
|
6
|
-
require "samuel/
|
7
|
-
require "samuel/
|
4
|
+
require "samuel/loader"
|
5
|
+
require "samuel/diary"
|
6
|
+
require "samuel/driver_patches/http_client"
|
7
|
+
require "samuel/driver_patches/net_http"
|
8
|
+
require "samuel/log_entries/base"
|
9
|
+
require "samuel/log_entries/http_client"
|
10
|
+
require "samuel/log_entries/net_http"
|
8
11
|
|
9
12
|
|
10
13
|
module Samuel
|
11
14
|
extend self
|
12
15
|
|
13
|
-
|
16
|
+
VERSION = "0.3.0"
|
17
|
+
|
18
|
+
attr_writer :logger, :config
|
14
19
|
|
15
20
|
def logger
|
16
21
|
@logger = nil if !defined?(@logger)
|
@@ -27,12 +32,6 @@ module Samuel
|
|
27
32
|
Thread.current[:__samuel_config] ? Thread.current[:__samuel_config] : @config
|
28
33
|
end
|
29
34
|
|
30
|
-
def log_request(http, request, &block)
|
31
|
-
request = Request.new(http, request, block)
|
32
|
-
request.perform_and_log!
|
33
|
-
request.response
|
34
|
-
end
|
35
|
-
|
36
35
|
def with_config(options = {})
|
37
36
|
original_config = config.dup
|
38
37
|
nested = !Thread.current[:__samuel_config].nil?
|
@@ -46,7 +45,8 @@ module Samuel
|
|
46
45
|
Thread.current[:__samuel_config] = nil
|
47
46
|
@config = {:label => nil, :labels => {"" => "HTTP"}, :filtered_params => []}
|
48
47
|
end
|
49
|
-
|
50
48
|
end
|
51
49
|
|
50
|
+
|
52
51
|
Samuel.reset_config
|
52
|
+
Samuel::Loader.apply_driver_patches
|
data/lib/samuel/diary.rb
ADDED
@@ -0,0 +1,29 @@
|
|
1
|
+
module Samuel
|
2
|
+
module Diary
|
3
|
+
extend self
|
4
|
+
|
5
|
+
def record_request(http, request, time_requested)
|
6
|
+
@requests ||= []
|
7
|
+
@requests.push({:request => request, :time_requested => time_requested})
|
8
|
+
end
|
9
|
+
|
10
|
+
def record_response(http, request, response, time_responded)
|
11
|
+
time_requested = @requests.detect { |r| r[:request] == request }[:time_requested]
|
12
|
+
@requests.reject! { |r| r[:request] == request }
|
13
|
+
log_request_and_response(http, request, response, time_requested, time_responded)
|
14
|
+
end
|
15
|
+
|
16
|
+
private
|
17
|
+
|
18
|
+
def log_request_and_response(http, request, response, time_started, time_ended)
|
19
|
+
log_entry_class = case http.class.to_s
|
20
|
+
when "Net::HTTP" then LogEntries::NetHttp
|
21
|
+
when "HTTPClient" then LogEntries::HttpClient
|
22
|
+
else raise NotImplementedError
|
23
|
+
end
|
24
|
+
log_entry = log_entry_class.new(http, request, response, time_started, time_ended)
|
25
|
+
log_entry.log!
|
26
|
+
end
|
27
|
+
|
28
|
+
end
|
29
|
+
end
|
@@ -0,0 +1,54 @@
|
|
1
|
+
module Samuel
|
2
|
+
module DriverPatches
|
3
|
+
|
4
|
+
module HTTPClient
|
5
|
+
def self.included(klass)
|
6
|
+
methods_to_wrap = %w(initialize do_get_block do_get_stream)
|
7
|
+
methods_to_wrap.each do |method|
|
8
|
+
klass.send(:alias_method, "#{method}_without_samuel", method)
|
9
|
+
klass.send(:alias_method, method, "#{method}_with_samuel")
|
10
|
+
end
|
11
|
+
end
|
12
|
+
|
13
|
+
def initialize_with_samuel(*args)
|
14
|
+
initialize_without_samuel(*args)
|
15
|
+
@request_filter << LoggingFilter.new(self)
|
16
|
+
end
|
17
|
+
|
18
|
+
def do_get_block_with_samuel(req, proxy, conn, &block)
|
19
|
+
begin
|
20
|
+
do_get_block_without_samuel(req, proxy, conn, &block)
|
21
|
+
rescue Exception => e
|
22
|
+
Samuel::Diary.record_response(self, req, e, Time.now)
|
23
|
+
raise
|
24
|
+
end
|
25
|
+
end
|
26
|
+
|
27
|
+
def do_get_stream_with_samuel(req, proxy, conn)
|
28
|
+
begin
|
29
|
+
do_get_stream_without_samuel(req, proxy, conn)
|
30
|
+
rescue Exception => e
|
31
|
+
Samuel::Diary.record_response(self, req, e, Time.now)
|
32
|
+
raise
|
33
|
+
end
|
34
|
+
end
|
35
|
+
|
36
|
+
class LoggingFilter
|
37
|
+
def initialize(http_client_instance)
|
38
|
+
@http_client_instance = http_client_instance
|
39
|
+
end
|
40
|
+
|
41
|
+
def filter_request(request)
|
42
|
+
Samuel::Diary.record_request(@http_client_instance, request, Time.now)
|
43
|
+
end
|
44
|
+
|
45
|
+
def filter_response(request, response)
|
46
|
+
Samuel::Diary.record_response(@http_client_instance, request, response, Time.now)
|
47
|
+
nil # this returns command symbols like :retry, etc.
|
48
|
+
end
|
49
|
+
end
|
50
|
+
end
|
51
|
+
|
52
|
+
end
|
53
|
+
end
|
54
|
+
|
@@ -0,0 +1,42 @@
|
|
1
|
+
module Samuel
|
2
|
+
module DriverPatches
|
3
|
+
|
4
|
+
module NetHTTP
|
5
|
+
def self.included(klass)
|
6
|
+
methods_to_wrap = %w(request connect)
|
7
|
+
methods_to_wrap.each do |method|
|
8
|
+
klass.send(:alias_method, "#{method}_without_samuel", method)
|
9
|
+
klass.send(:alias_method, method, "#{method}_with_samuel")
|
10
|
+
end
|
11
|
+
end
|
12
|
+
|
13
|
+
def request_with_samuel(request, body = nil, &block)
|
14
|
+
Samuel::Diary.record_request(self, request, Time.now)
|
15
|
+
|
16
|
+
response, exception_raised = nil, false
|
17
|
+
begin
|
18
|
+
response = request_without_samuel(request, body, &block)
|
19
|
+
rescue Exception => response
|
20
|
+
exception_raised = true
|
21
|
+
end
|
22
|
+
|
23
|
+
Samuel::Diary.record_response(self, request, response, Time.now)
|
24
|
+
|
25
|
+
raise response if exception_raised
|
26
|
+
response
|
27
|
+
end
|
28
|
+
|
29
|
+
def connect_with_samuel
|
30
|
+
connect_without_samuel
|
31
|
+
rescue Exception => response
|
32
|
+
fake_request = Object.new
|
33
|
+
def fake_request.path; ""; end
|
34
|
+
def fake_request.method; "CONNECT"; end
|
35
|
+
Samuel::Diary.record_request(self, fake_request, Time.now)
|
36
|
+
Samuel::Diary.record_response(self, fake_request, response, Time.now)
|
37
|
+
raise
|
38
|
+
end
|
39
|
+
end
|
40
|
+
|
41
|
+
end
|
42
|
+
end
|
@@ -0,0 +1,19 @@
|
|
1
|
+
module Samuel
|
2
|
+
module Loader
|
3
|
+
extend self
|
4
|
+
|
5
|
+
def apply_driver_patches
|
6
|
+
loaded = { :net_http => defined?(Net::HTTP),
|
7
|
+
:http_client => defined?(HTTPClient) }
|
8
|
+
|
9
|
+
Net::HTTP.send(:include, DriverPatches::NetHTTP) if loaded[:net_http]
|
10
|
+
HTTPClient.send(:include, DriverPatches::HTTPClient) if loaded[:http_client]
|
11
|
+
|
12
|
+
if loaded.values.none?
|
13
|
+
require 'net/http'
|
14
|
+
apply_driver_patches
|
15
|
+
end
|
16
|
+
end
|
17
|
+
|
18
|
+
end
|
19
|
+
end
|
@@ -0,0 +1,76 @@
|
|
1
|
+
module Samuel
|
2
|
+
module LogEntries
|
3
|
+
|
4
|
+
class Base
|
5
|
+
def initialize(http, request, response, time_requested, time_responded)
|
6
|
+
@http, @request, @response = http, request, response
|
7
|
+
@seconds = time_responded - time_requested
|
8
|
+
end
|
9
|
+
|
10
|
+
def log!
|
11
|
+
Samuel.logger.add(log_level, log_message)
|
12
|
+
end
|
13
|
+
|
14
|
+
|
15
|
+
protected
|
16
|
+
|
17
|
+
def log_message
|
18
|
+
bold = "\e[1m"
|
19
|
+
blue = "\e[34m"
|
20
|
+
underline = "\e[4m"
|
21
|
+
reset = "\e[0m"
|
22
|
+
" #{bold}#{blue}#{underline}#{label} request (#{milliseconds}ms) " +
|
23
|
+
"#{response_summary}#{reset} #{method} #{uri}"
|
24
|
+
end
|
25
|
+
|
26
|
+
def milliseconds
|
27
|
+
(@seconds * 1000).round
|
28
|
+
end
|
29
|
+
|
30
|
+
def uri
|
31
|
+
"#{scheme}://#{host}#{port_if_not_default}#{path}#{'?' if query}#{filtered_query}"
|
32
|
+
end
|
33
|
+
|
34
|
+
def label
|
35
|
+
return Samuel.config[:label] if Samuel.config[:label]
|
36
|
+
|
37
|
+
pair = Samuel.config[:labels].detect { |domain, label| host.include?(domain) }
|
38
|
+
pair[1] if pair
|
39
|
+
end
|
40
|
+
|
41
|
+
def response_summary
|
42
|
+
if @response.is_a?(Exception)
|
43
|
+
@response.class
|
44
|
+
else
|
45
|
+
"[#{status_code} #{status_message}]"
|
46
|
+
end
|
47
|
+
end
|
48
|
+
|
49
|
+
def log_level
|
50
|
+
error? ? Logger::WARN : Logger::INFO
|
51
|
+
end
|
52
|
+
|
53
|
+
def ssl?
|
54
|
+
scheme == 'https'
|
55
|
+
end
|
56
|
+
|
57
|
+
def filtered_query
|
58
|
+
return "" if query.nil?
|
59
|
+
patterns = [Samuel.config[:filtered_params]].flatten
|
60
|
+
patterns.map { |pattern|
|
61
|
+
pattern_for_regex = Regexp.escape(pattern.to_s)
|
62
|
+
[/([^&]*#{pattern_for_regex}[^&=]*)=(?:[^&]+)/, '\1=[FILTERED]']
|
63
|
+
}.inject(query) { |filtered, filter| filtered.gsub(*filter) }
|
64
|
+
end
|
65
|
+
|
66
|
+
def port_if_not_default
|
67
|
+
if (!ssl? && port == 80) || (ssl? && port == 443)
|
68
|
+
""
|
69
|
+
else
|
70
|
+
":#{port}"
|
71
|
+
end
|
72
|
+
end
|
73
|
+
end
|
74
|
+
|
75
|
+
end
|
76
|
+
end
|
@@ -0,0 +1,28 @@
|
|
1
|
+
module Samuel
|
2
|
+
module LogEntries
|
3
|
+
|
4
|
+
class HttpClient < Base
|
5
|
+
extend Forwardable
|
6
|
+
|
7
|
+
def_delegators :"@request.header.request_uri",
|
8
|
+
:host, :path, :query, :scheme, :port
|
9
|
+
|
10
|
+
def method
|
11
|
+
@request.header.request_method
|
12
|
+
end
|
13
|
+
|
14
|
+
def status_code
|
15
|
+
@response.status
|
16
|
+
end
|
17
|
+
|
18
|
+
def status_message
|
19
|
+
@response.header.reason_phrase.strip
|
20
|
+
end
|
21
|
+
|
22
|
+
def error?
|
23
|
+
@response.is_a?(Exception) || @response.status.to_s =~ /^(4|5)/
|
24
|
+
end
|
25
|
+
end
|
26
|
+
|
27
|
+
end
|
28
|
+
end
|
@@ -0,0 +1,45 @@
|
|
1
|
+
module Samuel
|
2
|
+
module LogEntries
|
3
|
+
|
4
|
+
class NetHttp < Base
|
5
|
+
def host
|
6
|
+
@http.address
|
7
|
+
end
|
8
|
+
|
9
|
+
def path
|
10
|
+
@request.path.split("?")[0]
|
11
|
+
end
|
12
|
+
|
13
|
+
def query
|
14
|
+
@request.path.split("?")[1]
|
15
|
+
end
|
16
|
+
|
17
|
+
def scheme
|
18
|
+
@http.use_ssl? ? "https" : "http"
|
19
|
+
end
|
20
|
+
|
21
|
+
def port
|
22
|
+
@http.port
|
23
|
+
end
|
24
|
+
|
25
|
+
def method
|
26
|
+
@request.method.to_s.upcase
|
27
|
+
end
|
28
|
+
|
29
|
+
def status_code
|
30
|
+
@response.code
|
31
|
+
end
|
32
|
+
|
33
|
+
def status_message
|
34
|
+
@response.message.strip
|
35
|
+
end
|
36
|
+
|
37
|
+
def error?
|
38
|
+
error_classes = %w(Exception Net::HTTPClientError Net::HTTPServerError)
|
39
|
+
response_ancestors = @response.class.ancestors.map { |a| a.to_s }
|
40
|
+
(error_classes & response_ancestors).any?
|
41
|
+
end
|
42
|
+
end
|
43
|
+
|
44
|
+
end
|
45
|
+
end
|
data/samuel.gemspec
CHANGED
@@ -1,16 +1,16 @@
|
|
1
1
|
# Generated by jeweler
|
2
|
-
# DO NOT EDIT THIS FILE
|
3
|
-
# Instead, edit Jeweler::Tasks in Rakefile, and run
|
2
|
+
# DO NOT EDIT THIS FILE DIRECTLY
|
3
|
+
# Instead, edit Jeweler::Tasks in Rakefile, and run the gemspec command
|
4
4
|
# -*- encoding: utf-8 -*-
|
5
5
|
|
6
6
|
Gem::Specification.new do |s|
|
7
7
|
s.name = %q{samuel}
|
8
|
-
s.version = "0.
|
8
|
+
s.version = "0.3.0"
|
9
9
|
|
10
10
|
s.required_rubygems_version = Gem::Requirement.new(">= 0") if s.respond_to? :required_rubygems_version=
|
11
11
|
s.authors = ["Chris Kampmeier"]
|
12
|
-
s.date = %q{
|
13
|
-
s.description = %q{An automatic logger for HTTP requests in Ruby
|
12
|
+
s.date = %q{2010-01-01}
|
13
|
+
s.description = %q{An automatic logger for HTTP requests in Ruby, supporting the Net::HTTP and HTTPClient client libraries.}
|
14
14
|
s.email = %q{chris@kampers.net}
|
15
15
|
s.extra_rdoc_files = [
|
16
16
|
"LICENSE",
|
@@ -22,12 +22,18 @@ Gem::Specification.new do |s|
|
|
22
22
|
"LICENSE",
|
23
23
|
"README.rdoc",
|
24
24
|
"Rakefile",
|
25
|
-
"VERSION",
|
26
25
|
"lib/samuel.rb",
|
27
|
-
"lib/samuel/
|
28
|
-
"lib/samuel/
|
26
|
+
"lib/samuel/diary.rb",
|
27
|
+
"lib/samuel/driver_patches/http_client.rb",
|
28
|
+
"lib/samuel/driver_patches/net_http.rb",
|
29
|
+
"lib/samuel/loader.rb",
|
30
|
+
"lib/samuel/log_entries/base.rb",
|
31
|
+
"lib/samuel/log_entries/http_client.rb",
|
32
|
+
"lib/samuel/log_entries/net_http.rb",
|
29
33
|
"samuel.gemspec",
|
30
|
-
"test/
|
34
|
+
"test/http_client_test.rb",
|
35
|
+
"test/loader_test.rb",
|
36
|
+
"test/net_http_test.rb",
|
31
37
|
"test/samuel_test.rb",
|
32
38
|
"test/test_helper.rb",
|
33
39
|
"test/thread_test.rb"
|
@@ -39,7 +45,9 @@ Gem::Specification.new do |s|
|
|
39
45
|
s.rubygems_version = %q{1.3.5}
|
40
46
|
s.summary = %q{An automatic logger for HTTP requests in Ruby}
|
41
47
|
s.test_files = [
|
42
|
-
"test/
|
48
|
+
"test/http_client_test.rb",
|
49
|
+
"test/loader_test.rb",
|
50
|
+
"test/net_http_test.rb",
|
43
51
|
"test/samuel_test.rb",
|
44
52
|
"test/test_helper.rb",
|
45
53
|
"test/thread_test.rb"
|
@@ -50,20 +58,21 @@ Gem::Specification.new do |s|
|
|
50
58
|
s.specification_version = 3
|
51
59
|
|
52
60
|
if Gem::Version.new(Gem::RubyGemsVersion) >= Gem::Version.new('1.2.0') then
|
53
|
-
s.add_development_dependency(%q<
|
54
|
-
s.add_development_dependency(%q<yard>, [">= 0"])
|
61
|
+
s.add_development_dependency(%q<shoulda>, [">= 0"])
|
55
62
|
s.add_development_dependency(%q<mocha>, [">= 0"])
|
63
|
+
s.add_development_dependency(%q<httpclient>, [">= 0"])
|
56
64
|
s.add_development_dependency(%q<fakeweb>, [">= 0"])
|
57
65
|
else
|
58
|
-
s.add_dependency(%q<
|
59
|
-
s.add_dependency(%q<yard>, [">= 0"])
|
66
|
+
s.add_dependency(%q<shoulda>, [">= 0"])
|
60
67
|
s.add_dependency(%q<mocha>, [">= 0"])
|
68
|
+
s.add_dependency(%q<httpclient>, [">= 0"])
|
61
69
|
s.add_dependency(%q<fakeweb>, [">= 0"])
|
62
70
|
end
|
63
71
|
else
|
64
|
-
s.add_dependency(%q<
|
65
|
-
s.add_dependency(%q<yard>, [">= 0"])
|
72
|
+
s.add_dependency(%q<shoulda>, [">= 0"])
|
66
73
|
s.add_dependency(%q<mocha>, [">= 0"])
|
74
|
+
s.add_dependency(%q<httpclient>, [">= 0"])
|
67
75
|
s.add_dependency(%q<fakeweb>, [">= 0"])
|
68
76
|
end
|
69
77
|
end
|
78
|
+
|
@@ -0,0 +1,97 @@
|
|
1
|
+
require 'test_helper'
|
2
|
+
|
3
|
+
class HttpClientTest < Test::Unit::TestCase
|
4
|
+
context "making an HTTPClient request" do
|
5
|
+
setup { setup_test_logger
|
6
|
+
start_test_server
|
7
|
+
Samuel.reset_config }
|
8
|
+
teardown { teardown_test_logger }
|
9
|
+
|
10
|
+
context "to GET http://localhost:8000/, responding with a 200 in 53ms" do
|
11
|
+
setup do
|
12
|
+
now = Time.now
|
13
|
+
Time.stubs(:now).returns(now, now + 0.053)
|
14
|
+
HTTPClient.get("http://localhost:8000/")
|
15
|
+
end
|
16
|
+
|
17
|
+
should_log_lines 1
|
18
|
+
should_log_at_level :info
|
19
|
+
should_log_including "HTTP request"
|
20
|
+
should_log_including "(53ms)"
|
21
|
+
should_log_including "[200 OK]"
|
22
|
+
should_log_including "GET http://localhost:8000/"
|
23
|
+
end
|
24
|
+
|
25
|
+
context "using PUT" do
|
26
|
+
setup do
|
27
|
+
HTTPClient.put("http://localhost:8000/books/1", "test=true")
|
28
|
+
end
|
29
|
+
|
30
|
+
should_log_including "PUT http://localhost:8000/books/1"
|
31
|
+
end
|
32
|
+
|
33
|
+
context "using an asynchronous POST" do
|
34
|
+
setup do
|
35
|
+
body = "title=Infinite%20Jest"
|
36
|
+
client = HTTPClient.new
|
37
|
+
connection = client.post_async("http://localhost:8000/books", body)
|
38
|
+
sleep 0.1 until connection.finished?
|
39
|
+
end
|
40
|
+
|
41
|
+
should_log_including "POST http://localhost:8000/books"
|
42
|
+
end
|
43
|
+
|
44
|
+
context "that raises" do
|
45
|
+
setup do
|
46
|
+
begin
|
47
|
+
HTTPClient.get("http://localhost:8001/")
|
48
|
+
rescue Errno::ECONNREFUSED => @exception
|
49
|
+
end
|
50
|
+
end
|
51
|
+
|
52
|
+
should_log_at_level :warn
|
53
|
+
should_log_including "HTTP request"
|
54
|
+
should_log_including "GET http://localhost:8001/"
|
55
|
+
should_log_including "Errno::ECONNREFUSED"
|
56
|
+
should_log_including %r|\d+ms|
|
57
|
+
should_raise_exception Errno::ECONNREFUSED
|
58
|
+
end
|
59
|
+
|
60
|
+
context "using an asynchronous GET that raises" do
|
61
|
+
setup do
|
62
|
+
begin
|
63
|
+
client = HTTPClient.new
|
64
|
+
connection = client.get_async("http://localhost:8001/")
|
65
|
+
sleep 0.1 until connection.finished?
|
66
|
+
rescue Errno::ECONNREFUSED => @exception
|
67
|
+
end
|
68
|
+
end
|
69
|
+
|
70
|
+
should_log_at_level :warn
|
71
|
+
should_log_including "HTTP request"
|
72
|
+
should_log_including "GET http://localhost:8001/"
|
73
|
+
should_log_including "Errno::ECONNREFUSED"
|
74
|
+
should_log_including %r|\d+ms|
|
75
|
+
should_raise_exception Errno::ECONNREFUSED
|
76
|
+
end
|
77
|
+
|
78
|
+
context "that responds with a 400-level code" do
|
79
|
+
setup do
|
80
|
+
HTTPClient.get("http://localhost:8000/test?404")
|
81
|
+
end
|
82
|
+
|
83
|
+
should_log_at_level :warn
|
84
|
+
should_log_including "[404 Not Found]"
|
85
|
+
end
|
86
|
+
|
87
|
+
context "that responds with a 500-level code" do
|
88
|
+
setup do
|
89
|
+
HTTPClient.get("http://localhost:8000/test?500")
|
90
|
+
end
|
91
|
+
|
92
|
+
should_log_at_level :warn
|
93
|
+
should_log_including "[500 Internal Server Error]"
|
94
|
+
end
|
95
|
+
end
|
96
|
+
|
97
|
+
end
|
data/test/loader_test.rb
ADDED
@@ -0,0 +1,63 @@
|
|
1
|
+
require 'test_helper'
|
2
|
+
|
3
|
+
class LoaderTest < Test::Unit::TestCase
|
4
|
+
|
5
|
+
def capture_output(code = "")
|
6
|
+
requires = @requires.map { |lib| "require '#{lib}';" }.join(' ')
|
7
|
+
samuel_dir = "#{File.dirname(__FILE__)}/../lib"
|
8
|
+
`#{ruby_path} -I#{samuel_dir} -e "#{requires} #{code}" 2>&1`
|
9
|
+
end
|
10
|
+
|
11
|
+
context "loading Samuel" do
|
12
|
+
setup do
|
13
|
+
start_test_server
|
14
|
+
@requires = ['samuel']
|
15
|
+
end
|
16
|
+
|
17
|
+
context "when no HTTP drivers are loaded" do
|
18
|
+
should "automatically load Net::HTTP" do
|
19
|
+
output = capture_output "puts defined?(Net::HTTP)"
|
20
|
+
assert_equal "constant", output.strip
|
21
|
+
end
|
22
|
+
|
23
|
+
should "successfully log a Net::HTTP request" do
|
24
|
+
output = capture_output "Net::HTTP.get(URI.parse('http://localhost:8000'))"
|
25
|
+
assert_match %r[HTTP request], output
|
26
|
+
end
|
27
|
+
|
28
|
+
should "not load HTTPClient" do
|
29
|
+
output = capture_output "puts defined?(HTTPClient)"
|
30
|
+
assert_equal "nil", output.strip
|
31
|
+
end
|
32
|
+
end
|
33
|
+
|
34
|
+
context "when Net::HTTP is already loaded" do
|
35
|
+
setup { @requires.unshift('net/http') }
|
36
|
+
|
37
|
+
should "successfully log a Net::HTTP request" do
|
38
|
+
output = capture_output "Net::HTTP.get(URI.parse('http://localhost:8000'))"
|
39
|
+
assert_match %r[HTTP request], output
|
40
|
+
end
|
41
|
+
|
42
|
+
should "not load HTTPClient" do
|
43
|
+
output = capture_output "puts defined?(HTTPClient)"
|
44
|
+
assert_match "nil", output.strip
|
45
|
+
end
|
46
|
+
end
|
47
|
+
|
48
|
+
context "when HTTPClient is already loaded" do
|
49
|
+
setup { @requires.unshift('rubygems', 'httpclient') }
|
50
|
+
|
51
|
+
should "successfully log an HTTPClient request" do
|
52
|
+
output = capture_output "HTTPClient.get('http://localhost:8000')"
|
53
|
+
assert_match %r[HTTP request], output
|
54
|
+
end
|
55
|
+
|
56
|
+
should "not load Net::HTTP" do
|
57
|
+
output = capture_output "puts defined?(Net::HTTP)"
|
58
|
+
assert_match "nil", output.strip
|
59
|
+
end
|
60
|
+
end
|
61
|
+
end
|
62
|
+
|
63
|
+
end
|
@@ -11,7 +11,8 @@ class RequestTest < Test::Unit::TestCase
|
|
11
11
|
context "to GET http://example.com/test, responding with a 200 in 53ms" do
|
12
12
|
setup do
|
13
13
|
FakeWeb.register_uri(:get, "http://example.com/test", :status => [200, "OK"])
|
14
|
-
|
14
|
+
now = Time.now
|
15
|
+
Time.stubs(:now).returns(now, now + 0.053)
|
15
16
|
open "http://example.com/test"
|
16
17
|
end
|
17
18
|
|
@@ -57,7 +58,7 @@ class RequestTest < Test::Unit::TestCase
|
|
57
58
|
FakeWeb.register_uri(:get, "http://example.com/test", :exception => Errno::ECONNREFUSED)
|
58
59
|
begin
|
59
60
|
Net::HTTP.start("example.com") { |http| http.get("/test") }
|
60
|
-
rescue
|
61
|
+
rescue Exception => @exception
|
61
62
|
end
|
62
63
|
end
|
63
64
|
|
@@ -69,6 +70,32 @@ class RequestTest < Test::Unit::TestCase
|
|
69
70
|
should_raise_exception Errno::ECONNREFUSED
|
70
71
|
end
|
71
72
|
|
73
|
+
context "that raises a SocketError when connecting" do
|
74
|
+
setup do
|
75
|
+
FakeWeb.allow_net_connect = true
|
76
|
+
begin
|
77
|
+
http = Net::HTTP.new("example.com")
|
78
|
+
# This is an implementation-dependent hack; it would be more correct
|
79
|
+
# to stub out TCPSocket.open, but I can't get Mocha to make it raise
|
80
|
+
# correctly. Maybe related to TCPSocket being native code?
|
81
|
+
http.stubs(:connect_without_samuel).raises(SocketError)
|
82
|
+
http.start { |h| h.get("/test") }
|
83
|
+
rescue Exception => @exception
|
84
|
+
end
|
85
|
+
end
|
86
|
+
|
87
|
+
teardown do
|
88
|
+
FakeWeb.allow_net_connect = false
|
89
|
+
end
|
90
|
+
|
91
|
+
should_log_at_level :warn
|
92
|
+
should_log_including "HTTP request"
|
93
|
+
should_log_including "CONNECT http://example.com"
|
94
|
+
should_log_including "SocketError"
|
95
|
+
should_log_including %r|\d+ms|
|
96
|
+
should_raise_exception SocketError
|
97
|
+
end
|
98
|
+
|
72
99
|
context "that responds with a 500-level code" do
|
73
100
|
setup do
|
74
101
|
FakeWeb.register_uri(:get, "http://example.com/test", :status => [502, "Bad Gateway"])
|
data/test/test_helper.rb
CHANGED
@@ -1,16 +1,19 @@
|
|
1
1
|
require 'rubygems'
|
2
|
-
|
2
|
+
|
3
3
|
require 'shoulda'
|
4
4
|
require 'mocha'
|
5
|
+
|
6
|
+
require 'net/http'
|
7
|
+
require 'httpclient'
|
8
|
+
|
5
9
|
require 'open-uri'
|
6
10
|
require 'fakeweb'
|
11
|
+
require 'webrick'
|
7
12
|
|
8
|
-
FakeWeb.allow_net_connect = false
|
9
|
-
|
10
|
-
$LOAD_PATH.unshift(File.join(File.dirname(__FILE__), '..', 'lib'))
|
11
|
-
$LOAD_PATH.unshift(File.dirname(__FILE__))
|
12
13
|
require 'samuel'
|
13
14
|
|
15
|
+
FakeWeb.allow_net_connect = false
|
16
|
+
|
14
17
|
class Test::Unit::TestCase
|
15
18
|
TEST_LOG_PATH = File.join(File.dirname(__FILE__), 'test.log')
|
16
19
|
|
@@ -54,6 +57,12 @@ class Test::Unit::TestCase
|
|
54
57
|
end
|
55
58
|
end
|
56
59
|
|
60
|
+
# The path to the current ruby interpreter. Adapted from Rake's FileUtils.
|
61
|
+
def ruby_path
|
62
|
+
ext = ((RbConfig::CONFIG['ruby_install_name'] =~ /\.(com|cmd|exe|bat|rb|sh)$/) ? "" : RbConfig::CONFIG['EXEEXT'])
|
63
|
+
File.join(RbConfig::CONFIG['bindir'], RbConfig::CONFIG['ruby_install_name'] + ext).sub(/.*\s.*/m, '"\&"')
|
64
|
+
end
|
65
|
+
|
57
66
|
def setup_test_logger
|
58
67
|
FileUtils.rm_rf TEST_LOG_PATH
|
59
68
|
FileUtils.touch TEST_LOG_PATH
|
@@ -63,4 +72,26 @@ class Test::Unit::TestCase
|
|
63
72
|
def teardown_test_logger
|
64
73
|
FileUtils.rm_rf TEST_LOG_PATH
|
65
74
|
end
|
75
|
+
|
76
|
+
def start_test_server
|
77
|
+
return if defined?(@@server)
|
78
|
+
|
79
|
+
@@server = WEBrick::HTTPServer.new(
|
80
|
+
:Port => 8000, :AccessLog => [],
|
81
|
+
:Logger => WEBrick::Log.new(nil, WEBrick::BasicLog::WARN)
|
82
|
+
)
|
83
|
+
@@server.mount "/", ResponseCodeServer
|
84
|
+
at_exit { @@server.shutdown }
|
85
|
+
Thread.new { @@server.start }
|
86
|
+
end
|
87
|
+
end
|
88
|
+
|
89
|
+
class ResponseCodeServer < WEBrick::HTTPServlet::AbstractServlet
|
90
|
+
def do_GET(request, response)
|
91
|
+
response_code = request.query_string.nil? ? 200 : request.query_string.to_i
|
92
|
+
response.status = response_code
|
93
|
+
end
|
94
|
+
alias_method :do_POST, :do_GET
|
95
|
+
alias_method :do_PUT, :do_GET
|
96
|
+
alias_method :do_DELETE, :do_GET
|
66
97
|
end
|
metadata
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: samuel
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.
|
4
|
+
version: 0.3.0
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Chris Kampmeier
|
@@ -9,11 +9,11 @@ autorequire:
|
|
9
9
|
bindir: bin
|
10
10
|
cert_chain: []
|
11
11
|
|
12
|
-
date:
|
12
|
+
date: 2010-01-01 00:00:00 -08:00
|
13
13
|
default_executable:
|
14
14
|
dependencies:
|
15
15
|
- !ruby/object:Gem::Dependency
|
16
|
-
name:
|
16
|
+
name: shoulda
|
17
17
|
type: :development
|
18
18
|
version_requirement:
|
19
19
|
version_requirements: !ruby/object:Gem::Requirement
|
@@ -23,7 +23,7 @@ dependencies:
|
|
23
23
|
version: "0"
|
24
24
|
version:
|
25
25
|
- !ruby/object:Gem::Dependency
|
26
|
-
name:
|
26
|
+
name: mocha
|
27
27
|
type: :development
|
28
28
|
version_requirement:
|
29
29
|
version_requirements: !ruby/object:Gem::Requirement
|
@@ -33,7 +33,7 @@ dependencies:
|
|
33
33
|
version: "0"
|
34
34
|
version:
|
35
35
|
- !ruby/object:Gem::Dependency
|
36
|
-
name:
|
36
|
+
name: httpclient
|
37
37
|
type: :development
|
38
38
|
version_requirement:
|
39
39
|
version_requirements: !ruby/object:Gem::Requirement
|
@@ -52,7 +52,7 @@ dependencies:
|
|
52
52
|
- !ruby/object:Gem::Version
|
53
53
|
version: "0"
|
54
54
|
version:
|
55
|
-
description: An automatic logger for HTTP requests in Ruby
|
55
|
+
description: An automatic logger for HTTP requests in Ruby, supporting the Net::HTTP and HTTPClient client libraries.
|
56
56
|
email: chris@kampers.net
|
57
57
|
executables: []
|
58
58
|
|
@@ -67,12 +67,18 @@ files:
|
|
67
67
|
- LICENSE
|
68
68
|
- README.rdoc
|
69
69
|
- Rakefile
|
70
|
-
- VERSION
|
71
70
|
- lib/samuel.rb
|
72
|
-
- lib/samuel/
|
73
|
-
- lib/samuel/
|
71
|
+
- lib/samuel/diary.rb
|
72
|
+
- lib/samuel/driver_patches/http_client.rb
|
73
|
+
- lib/samuel/driver_patches/net_http.rb
|
74
|
+
- lib/samuel/loader.rb
|
75
|
+
- lib/samuel/log_entries/base.rb
|
76
|
+
- lib/samuel/log_entries/http_client.rb
|
77
|
+
- lib/samuel/log_entries/net_http.rb
|
74
78
|
- samuel.gemspec
|
75
|
-
- test/
|
79
|
+
- test/http_client_test.rb
|
80
|
+
- test/loader_test.rb
|
81
|
+
- test/net_http_test.rb
|
76
82
|
- test/samuel_test.rb
|
77
83
|
- test/test_helper.rb
|
78
84
|
- test/thread_test.rb
|
@@ -105,7 +111,9 @@ signing_key:
|
|
105
111
|
specification_version: 3
|
106
112
|
summary: An automatic logger for HTTP requests in Ruby
|
107
113
|
test_files:
|
108
|
-
- test/
|
114
|
+
- test/http_client_test.rb
|
115
|
+
- test/loader_test.rb
|
116
|
+
- test/net_http_test.rb
|
109
117
|
- test/samuel_test.rb
|
110
118
|
- test/test_helper.rb
|
111
119
|
- test/thread_test.rb
|
data/VERSION
DELETED
@@ -1 +0,0 @@
|
|
1
|
-
0.2.1
|
data/lib/samuel/net_http.rb
DELETED
data/lib/samuel/request.rb
DELETED
@@ -1,96 +0,0 @@
|
|
1
|
-
module Samuel
|
2
|
-
class Request
|
3
|
-
|
4
|
-
attr_accessor :response
|
5
|
-
|
6
|
-
def initialize(http, request, proc)
|
7
|
-
@http, @request, @proc = http, request, proc
|
8
|
-
end
|
9
|
-
|
10
|
-
def perform_and_log!
|
11
|
-
# If an exception is raised in the Benchmark block, it'll interrupt the
|
12
|
-
# benchmark. Instead, use an inner block to record it as the "response"
|
13
|
-
# for raising after the benchmark (and logging) is done.
|
14
|
-
@seconds = Benchmark.realtime do
|
15
|
-
begin; @response = @proc.call; rescue Exception => @response; end
|
16
|
-
end
|
17
|
-
Samuel.logger.add(log_level, log_message)
|
18
|
-
raise @response if @response.is_a?(Exception)
|
19
|
-
end
|
20
|
-
|
21
|
-
private
|
22
|
-
|
23
|
-
def log_message
|
24
|
-
bold = "\e[1m"
|
25
|
-
blue = "\e[34m"
|
26
|
-
underline = "\e[4m"
|
27
|
-
reset = "\e[0m"
|
28
|
-
" #{bold}#{blue}#{underline}#{label} request (#{milliseconds}ms) " +
|
29
|
-
"#{response_summary}#{reset} #{method} #{uri}"
|
30
|
-
end
|
31
|
-
|
32
|
-
def milliseconds
|
33
|
-
(@seconds * 1000).round
|
34
|
-
end
|
35
|
-
|
36
|
-
def uri
|
37
|
-
"#{scheme}://#{@http.address}#{port_if_not_default}#{filtered_path}"
|
38
|
-
end
|
39
|
-
|
40
|
-
def filtered_path
|
41
|
-
path_without_query, query = @request.path.split("?")
|
42
|
-
if query
|
43
|
-
patterns = [Samuel.config[:filtered_params]].flatten
|
44
|
-
patterns.map { |pattern|
|
45
|
-
pattern_for_regex = Regexp.escape(pattern.to_s)
|
46
|
-
[/([^&]*#{pattern_for_regex}[^&=]*)=(?:[^&]+)/, '\1=[FILTERED]']
|
47
|
-
}.each { |filter| query.gsub!(*filter) }
|
48
|
-
"#{path_without_query}?#{query}"
|
49
|
-
else
|
50
|
-
@request.path
|
51
|
-
end
|
52
|
-
end
|
53
|
-
|
54
|
-
def scheme
|
55
|
-
@http.use_ssl? ? "https" : "http"
|
56
|
-
end
|
57
|
-
|
58
|
-
def port_if_not_default
|
59
|
-
ssl, port = @http.use_ssl?, @http.port
|
60
|
-
if (!ssl && port == 80) || (ssl && port == 443)
|
61
|
-
""
|
62
|
-
else
|
63
|
-
":#{port}"
|
64
|
-
end
|
65
|
-
end
|
66
|
-
|
67
|
-
def method
|
68
|
-
@request.method.to_s.upcase
|
69
|
-
end
|
70
|
-
|
71
|
-
def label
|
72
|
-
return Samuel.config[:label] if Samuel.config[:label]
|
73
|
-
|
74
|
-
pair = Samuel.config[:labels].detect { |domain, label| @http.address.include?(domain) }
|
75
|
-
pair[1] if pair
|
76
|
-
end
|
77
|
-
|
78
|
-
def response_summary
|
79
|
-
if response.is_a?(Exception)
|
80
|
-
response.class
|
81
|
-
else
|
82
|
-
"[#{response.code} #{response.message}]"
|
83
|
-
end
|
84
|
-
end
|
85
|
-
|
86
|
-
def log_level
|
87
|
-
error_classes = [Exception, Net::HTTPClientError, Net::HTTPServerError]
|
88
|
-
if error_classes.any? { |klass| response.is_a?(klass) }
|
89
|
-
level = Logger::WARN
|
90
|
-
else
|
91
|
-
level = Logger::INFO
|
92
|
-
end
|
93
|
-
end
|
94
|
-
|
95
|
-
end
|
96
|
-
end
|