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 CHANGED
@@ -1,4 +1,4 @@
1
- Copyright 2009 Chris Kampmeier
1
+ Copyright 2009–2010 Chris Kampmeier
2
2
 
3
3
  Permission is hereby granted, free of charge, to any person obtaining
4
4
  a copy of this software and associated documentation files (the
@@ -1,13 +1,16 @@
1
1
  = Samuel
2
2
 
3
- Samuel is a gem for automatic logging of your Net::HTTP requests. It's named for
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
- == Usage:
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
- * +:labels+ - This is a hash with domain substrings as keys and log labels as
48
- values. If a request domain includes one of the domain substrings, the
49
- corresponding label will be used for the first part of that log entry. By
50
- default this is set to <tt>\{"" => "HTTP"}</tt>, so that all requests are
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
- * +:label+ - As an alternative to the +:labels+ hash, this is simply a string.
53
- If set, it takes precedence over any +:labels+ (by default, it's not set). It
54
- gets <tt>"Request"</tt> appended to it as well -- so if you want your log to
55
- always say +Twitter API Request+ instead of the default +HTTP Request+, you
56
- can set this to <tt>"Twitter API"</tt>. I'd recommend using this setting
57
- globally if you're only making requests to one service, or inline if you just
58
- need to temporarily override the global +:labels+.
59
- * +:filtered_params+ - This works just like Rails's +filter_parameter_logging+
60
- method. Set it to a symbol, string, or array of them, and Samuel will filter
61
- the value of query parameters that have any of these patterns as a substring
62
- by replacing the value with <tt>[FILTERED]</tt> in your logs. By default, no
63
- filtering is enabled.
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. Adds Net::HTTP request logging to your Rails logs, and more.}
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 "thoughtbot-shoulda"
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: sudo gem install jeweler"
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: sudo gem install spicycode-rcov"
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: sudo gem install yard"
57
+ abort "YARD is not available. In order to run yardoc, you must: gem install yard"
61
58
  end
62
59
  end
@@ -1,16 +1,21 @@
1
1
  require "logger"
2
- require "net/http"
3
- require "net/https"
4
- require "benchmark"
2
+ require "forwardable"
5
3
 
6
- require "samuel/net_http"
7
- require "samuel/request"
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
- attr_writer :config, :logger
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
@@ -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
@@ -1,16 +1,16 @@
1
1
  # Generated by jeweler
2
- # DO NOT EDIT THIS FILE
3
- # Instead, edit Jeweler::Tasks in Rakefile, and run `rake gemspec`
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.2.1"
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{2009-09-15}
13
- s.description = %q{An automatic logger for HTTP requests in Ruby. Adds Net::HTTP request logging to your Rails logs, and more.}
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/net_http.rb",
28
- "lib/samuel/request.rb",
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/request_test.rb",
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/request_test.rb",
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<thoughtbot-shoulda>, [">= 0"])
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<thoughtbot-shoulda>, [">= 0"])
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<thoughtbot-shoulda>, [">= 0"])
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
@@ -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
- Benchmark.stubs(:realtime).yields.returns(0.053)
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 Errno::ECONNREFUSED => @exception
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"])
@@ -1,16 +1,19 @@
1
1
  require 'rubygems'
2
- require 'test/unit'
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.2.1
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: 2009-09-15 00:00:00 -07:00
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: thoughtbot-shoulda
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: yard
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: mocha
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. Adds Net::HTTP request logging to your Rails logs, and more.
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/net_http.rb
73
- - lib/samuel/request.rb
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/request_test.rb
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/request_test.rb
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
@@ -1,10 +0,0 @@
1
- class Net::HTTP
2
-
3
- alias request_without_samuel request
4
- def request(req, body = nil, &block)
5
- Samuel.log_request(self, req) do
6
- request_without_samuel(req, body, &block)
7
- end
8
- end
9
-
10
- end
@@ -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