samuel 0.2.1 → 0.3.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/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
|