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 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