public
Description: A very fast & simple Ruby web server
Homepage: http://code.macournoyer.com/thin/
Clone URL: git://github.com/macournoyer/thin.git
Search Repo:
Merge branch 'master' into keepalive

Conflicts:

  CHANGELOG
  example/adapter.rb
  lib/thin/server.rb
  lib/thin/version.rb
macournoyer (author)
Mon Jan 28 14:45:43 -0800 2008
commit  02b5468eb7eb02c1ceb267bcd77f2be4c708211d
tree    cabda9bea4eb619f294374cbc9f14d988f8100b4
parent  487c0020c63f1fd629fce48108329d8c5716b25b parent  5d29f05e15440c52447a12262de9450adcf8761b
...
1
2
3
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
4
5
6
...
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
0
@@ -1,6 +1,29 @@
0
 == 0.7.0 Bionic Pickle release
0
  * Persistent connection (keep-alive) & HTTP pipelining support
0
 
0
+== 0.6.2 Rambo release
0
+ * Add PID and more info from the last request to the Stats adapter
0
+ mostly taken from Rack::ShowException.
0
+ * pid and log files in cluster are no longer required to be relative to the
0
+ app directory (chdir option), fixes #24
0
+ * Revert to using #each when building response headers under Ruby 1.8,
0
+ solves an issue w/ Camping adapter, fixes #22
0
+ * Restructure thin script options in 3 sections: server, daemon and cluster
0
+ * Add --only (-o) option to control only one server of a cluster.
0
+ * Stylize stats page and make the url configurable from the thin script.
0
+ * Raise error if attempting to use unix sockets on windows.
0
+ * Add example config files for http://www.tildeslash.com/monit useage.
0
+ Include the example file using "include /path/to/thin/monit/file" in your monitrc file.
0
+ The group settings let you do this to manage your clusters:
0
+
0
+ sudo monit -g blog restart all
0
+
0
+ There are examples of thin listening on sockets and thin listening on unix sockets.
0
+
0
+== 0.6.1 Cheesecake release
0
+ * Remove socket file when server stops.
0
+ * Set back cluster to use 'thin' command to launch servers.
0
+
0
 == 0.6.0 Big Pony release
0
  * Add support for connection through UNIX domain socket.
0
    Use the --socket (-S) option w/ the thin script to configure the socket filename.
...
9
10
11
 
12
13
 
...
9
10
11
12
13
 
14
0
@@ -9,6 +9,7 @@
0
 require File.dirname(__FILE__) + '/utils'
0
 
0
 request = (ARGV[0] || 1000).to_i # Number of request to send (ab -n option)
0
+output_type = (ARGV[1] || 'print')
0
 
0
-benchmark %w(WEBrick Mongrel EMongrel Thin), request
0
+benchmark output_type, %w(WEBrick Mongrel EMongrel Thin), request, [1, 10, 20, 30, 40, 50, 60, 70, 80, 90, 100]
...
1
2
3
 
 
4
5
6
7
8
9
10
...
22
23
24
25
 
26
27
28
29
30
 
31
32
33
34
35
36
37
 
38
39
 
40
41
42
43
44
45
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
46
47
48
...
1
2
3
4
5
6
7
8
9
10
11
12
...
24
25
26
 
27
28
29
30
31
 
32
33
34
35
36
37
 
 
38
39
 
40
41
42
43
 
 
 
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
0
@@ -1,6 +1,8 @@
0
 require 'rack/lobster'
0
 
0
 def run(handler_name, n=1000, c=1)
0
+ port = 7000
0
+
0
   server = fork do
0
     [STDOUT, STDERR].each { |o| o.reopen "/dev/null" }
0
       
0
0
0
0
0
@@ -22,27 +24,49 @@
0
     app = Rack::Lobster.new
0
     
0
     handler = Rack::Handler.const_get(handler_name)
0
- handler.run app, :Host => '0.0.0.0', :Port => 7000
0
+ handler.run app, :Host => '0.0.0.0', :Port => port
0
   end
0
 
0
   sleep 2
0
 
0
- out = `nice -n20 ab -c #{c} -n #{n} http://127.0.0.1:7000/ 2> /dev/null`
0
+ out = `nice -n20 ab -c #{c} -n #{n} http://127.0.0.1:port/ 2> /dev/null`
0
 
0
   Process.kill('SIGKILL', server)
0
   Process.wait
0
   
0
   if requests = out.match(/^Requests.+?(\d+\.\d+)/)
0
- failed = out.match(/^Failed requests.+?(\d+)$/)[1]
0
- "#{requests[1].to_s.ljust(9)} #{failed}"
0
+ requests[1].to_i
0
   else
0
- 'ERROR'
0
+ 0
0
   end
0
 end
0
 
0
-def benchmark(servers, request, concurrency_levels=[1, 10, 100])
0
- puts 'server request concurrency req/s failures'
0
- puts '=' * 53
0
+def benchmark(type, servers, request, concurrency_levels)
0
+ send "#{type}_benchmark", servers, request, concurrency_levels
0
+end
0
+
0
+def graph_benchmark(servers, request, concurrency_levels)
0
+ require '/usr/local/lib/ruby/gems/1.8/gems/gruff-0.2.9/lib/gruff'
0
+ g = Gruff::Area.new
0
+ g.title = "Server benchmark"
0
+
0
+ servers.each do |server|
0
+ g.data(server, concurrency_levels.collect { |c| print '.'; run(server, request, c) })
0
+ end
0
+ puts
0
+
0
+ g.x_axis_label = 'Concurrency'
0
+ g.y_axis_label = 'Requests / sec'
0
+ g.labels = {}
0
+ concurrency_levels.each_with_index { |c, i| g.labels[i] = c.to_s }
0
+
0
+ g.write('bench.png')
0
+ `open bench.png`
0
+end
0
+
0
+def print_benchmark(servers, request, concurrency_levels)
0
+ puts 'server request concurrency req/s'
0
+ puts '=' * 42
0
   concurrency_levels.each do |c|
0
     servers.each do |server|
0
       puts "#{server.ljust(8)} #{request} #{c.to_s.ljust(4)} #{run(server, request, c)}"
...
34
35
36
37
38
 
 
 
 
 
 
 
 
39
40
41
42
43
44
45
46
47
48
49
50
51
52
 
 
 
 
 
 
 
 
53
54
55
...
62
63
64
65
 
66
67
68
...
101
102
103
104
 
105
106
107
...
34
35
36
 
 
37
38
39
40
41
42
43
44
45
46
47
48
49
 
 
50
51
 
 
 
52
53
54
55
56
57
58
59
60
61
62
63
64
...
71
72
73
 
74
75
76
77
...
110
111
112
 
113
114
115
116
0
@@ -34,22 +34,31 @@
0
   opts.on("-S", "--socket PATH", "bind to unix domain socket") { |file| options[:socket] = file }
0
   opts.on("-e", "--environment ENV", "Rails environment (default: development)") { |env| options[:environment] = env }
0
   opts.on("-c", "--chdir PATH", "Change to dir before starting") { |dir| options[:chdir] = File.expand_path(dir) }
0
- opts.on("-s", "--servers NUM", "Number of servers to start",
0
- "set a value >1 to start a cluster") { |num| options[:servers] = num.to_i }
0
+ opts.on("-t", "--timeout SEC", "Request or command timeout in sec",
0
+ "(default: #{options[:timeout]})") { |sec| options[:timeout] = sec.to_i }
0
+ opts.on( "--prefix PATH", "Mount the app under PATH (start with /)") { |path| options[:prefix] = path }
0
+ opts.on( "--stats PATH", "Mount the Stats adapter under PATH") { |path| options[:stats] = path }
0
+
0
+ opts.separator ""
0
+ opts.separator "Daemon options:"
0
+
0
   opts.on("-d", "--daemonize", "Run daemonized in the background") { options[:daemonize] = true }
0
   opts.on("-l", "--log FILE", "File to redirect output",
0
                               "(default: #{options[:log]})") { |file| options[:log] = file }
0
   opts.on("-P", "--pid FILE", "File to store PID",
0
                               "(default: #{options[:pid]})") { |file| options[:pid] = file }
0
- opts.on("-t", "--timeout SEC", "Request or command timeout in sec",
0
- "(default: #{options[:timeout]})") { |sec| options[:timeout] = sec.to_i }
0
   opts.on("-u", "--user NAME", "User to run daemon as (use with -g)") { |user| options[:user] = user }
0
   opts.on("-g", "--group NAME", "Group to run daemon as (use with -u)") { |group| options[:group] = group }
0
- opts.on( "--prefix PATH", "Mount the app under PATH (start with /)") { |path| options[:prefix] = path }
0
- opts.on("-C", "--config PATH", "Load option from a config file") { |file| options[:config] = file }
0
- opts.on( "--stats", "Install the Stats adapter at /stats") { options[:stats] = true }
0
   
0
   opts.separator ""
0
+ opts.separator "Cluster options:"
0
+
0
+ opts.on("-s", "--servers NUM", "Number of servers to start",
0
+ "set a value >1 to start a cluster") { |num| options[:servers] = num.to_i }
0
+ opts.on("-o", "--only NUM", "Send command to only one server of the cluster") { |only| options[:only] = only }
0
+ opts.on("-C", "--config PATH", "Load options from a config file") { |file| options[:config] = file }
0
+
0
+ opts.separator ""
0
   opts.separator "Common options:"
0
 
0
   opts.on_tail("-D", "--debug", "Set debbuging on") { $DEBUG = true }
0
@@ -62,7 +71,7 @@
0
 # == Utilities
0
 
0
 def cluster?(options)
0
- options[:servers] && options[:servers] > 1
0
+ options[:only] || (options[:servers] && options[:servers] > 1)
0
 end
0
 
0
 def load_options_from_config_file!(options)
0
@@ -101,7 +110,7 @@
0
     server.app = Rack::URLMap.new(options[:prefix] => server.app) if options[:prefix]
0
     
0
     # If a stats are required, wrap in Stats adapter
0
- server.app = Thin::Stats::Adapter.new(server.app) if options[:stats]
0
+ server.app = Thin::Stats::Adapter.new(server.app, options[:stats]) if options[:stats]
0
     
0
     server.start!
0
   end
...
 
 
 
1
2
3
4
...
14
15
16
17
18
 
 
 
 
 
 
 
 
 
19
20
 
 
 
 
 
 
...
1
2
3
4
5
6
7
...
17
18
19
 
 
20
21
22
23
24
25
26
27
28
29
 
30
31
32
33
34
35
0
@@ -1,3 +1,6 @@
0
+# Run with: ruby adapter.rb
0
+# Then browse to http://localhost:3000/test
0
+# and http://localhost:3000/files/adapter.rb
0
 require File.dirname(__FILE__) + '/../lib/thin'
0
 
0
 class SimpleAdapter
0
0
@@ -14,8 +17,20 @@
0
   end
0
 end
0
 
0
-app = Rack::URLMap.new('/test' => SimpleAdapter.new,
0
- '/files' => Rack::File.new('.'))
0
+Thin::Server.start('0.0.0.0', 3000) do
0
+ use Rack::CommonLogger
0
+ map '/test' do
0
+ run SimpleAdapter.new
0
+ end
0
+ map '/files' do
0
+ run Rack::File.new('.')
0
+ end
0
+end
0
 
0
-Thin::Server.start('0.0.0.0', 3000, app)
0
+# You could also start the server like this:
0
+#
0
+# app = Rack::URLMap.new('/test' => SimpleAdapter.new,
0
+# '/files' => Rack::File.new('.'))
0
+# Thin::Server.new('0.0.0.0', 3000, app).start
0
+#
...
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
...
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
0
@@ -1 +1,20 @@
0
+check process blog1
0
+ with pidfile /u/apps/blog/shared/pids/thin.14000.pid
0
+ start program = "/usr/local/bin/ruby /usr/local/bin/thin start -d -e production -u nobody -g nobody -p 14000 -a 127.0.0.1 -P tmp/pids/thin.14000.pid -c /u/apps/blog/current"
0
+ stop program = "/usr/local/bin/ruby /usr/local/bin/thin stop -P /u/apps/blog/shared/pids/thin.14000.pid"
0
+ if totalmem > 90.0 MB for 5 cycles then restart
0
+ if failed port 14000 then restart
0
+ if cpu usage > 95% for 3 cycles then restart
0
+ if 5 restarts within 5 cycles then timeout
0
+ group blog
0
+
0
+check process blog2
0
+ with pidfile /u/apps/blog/shared/pids/thin.14001.pid
0
+ start program = "/usr/local/bin/ruby /usr/local/bin/thin start -d -e production -u nobody -g nobody -p 14001 -a 127.0.0.1 -P tmp/pids/thin.14001.pid -c /u/apps/blog/current"
0
+ stop program = "/usr/local/bin/ruby /usr/local/bin/thin stop -P /u/apps/blog/shared/pids/thin.14001.pid"
0
+ if totalmem > 90.0 MB for 5 cycles then restart
0
+ if failed port 14001 then restart
0
+ if cpu usage > 95% for 3 cycles then restart
0
+ if 5 restarts within 5 cycles then timeout
0
+ group blog
...
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
...
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
0
@@ -1 +1,20 @@
0
+check process blog1
0
+ with pidfile /u/apps/blog/shared/pids/thin.1.pid
0
+ start program = "/usr/local/bin/ruby /usr/local/bin/thin start -d -e production -S /u/apps/blog/shared/pids/thin.1.sock -P tmp/pids/thin.1.pid -c /u/apps/blog/current"
0
+ stop program = "/usr/local/bin/ruby /usr/local/bin/thin stop -P /u/apps/blog/shared/pids/thin.1.pid"
0
+ if totalmem > 90.0 MB for 5 cycles then restart
0
+ if failed unixsocket /u/apps/blog/shared/pids/thin.1.sock then restart
0
+ if cpu usage > 95% for 3 cycles then restart
0
+ if 5 restarts within 5 cycles then timeout
0
+ group blog
0
+
0
+check process blog2
0
+ with pidfile /u/apps/blog/shared/pids/thin.2.pid
0
+ start program = "/usr/local/bin/ruby /usr/local/bin/thin start -d -e production -S /u/apps/blog/shared/pids/thin.2.sock -P tmp/pids/thin.2.pid -c /u/apps/blog/current"
0
+ stop program = "/usr/local/bin/ruby /usr/local/bin/thin stop -P /u/apps/blog/shared/pids/thin.2.pid"
0
+ if totalmem > 90.0 MB for 5 cycles then restart
0
+ if failed unixsocket /u/apps/blog/shared/pids/thin.2.sock then restart
0
+ if cpu usage > 95% for 3 cycles then restart
0
+ if 5 restarts within 5 cycles then timeout
0
+ group blog
...
1
2
3
4
5
 
 
6
7
8
...
20
21
22
23
 
 
24
25
26
...
31
32
33
34
35
 
 
36
37
38
...
118
119
120
121
122
 
 
 
 
 
 
123
124
125
...
1
2
3
 
 
4
5
6
7
8
...
20
21
22
 
23
24
25
26
27
...
32
33
34
 
 
35
36
37
38
39
...
119
120
121
 
 
122
123
124
125
126
127
128
129
130
0
@@ -1,8 +1,8 @@
0
 module Thin
0
   # Control a set of servers.
0
   # * Generate start and stop commands and run them.
0
- # * Inject the port number in the pid and log filenames.
0
- # Servers are started throught the +thin+ commandline script.
0
+ # * Inject the port or socket number in the pid and log filenames.
0
+ # Servers are started throught the +thin+ command-line script.
0
   class Cluster
0
     include Logging
0
     
0
@@ -20,7 +20,8 @@
0
     def initialize(options)
0
       @options = options.merge(:daemonize => true)
0
       @size = @options.delete(:servers)
0
- @script = File.join(File.dirname(__FILE__), '..', '..', 'bin', 'thin')
0
+ @only = @options.delete(:only)
0
+ @script = 'thin'
0
       
0
       if socket
0
         @options.delete(:address)
0
@@ -31,8 +32,8 @@
0
     def first_port; @options[:port] end
0
     def address; @options[:address] end
0
     def socket; @options[:socket] end
0
- def pid_file; File.expand_path File.join(@options[:chdir], @options[:pid]) end
0
- def log_file; File.expand_path File.join(@options[:chdir], @options[:log]) end
0
+ def pid_file; @options[:pid] end
0
+ def log_file; @options[:log] end
0
     
0
     # Start the servers
0
     def start
0
@@ -118,8 +119,12 @@
0
       end
0
       
0
       def with_each_server
0
- @size.times do |n|
0
- yield socket ? n : (first_port + n)
0
+ if @only
0
+ yield @only
0
+ else
0
+ @size.times do |n|
0
+ yield socket ? n : (first_port + n)
0
+ end
0
         end
0
       end
0
       
...
31
32
33
34
 
35
36
37
...
50
51
52
53
54
55
56
...
94
95
96
97
98
99
100
101
102
103
...
31
32
33
 
34
35
36
37
...
50
51
52
 
53
54
55
...
93
94
95
 
 
 
 
96
97
98
0
@@ -31,7 +31,7 @@
0
     
0
     # Turns the current script into a daemon process that detaches from the console.
0
     def daemonize
0
- check_plateform_support
0
+ raise PlatformNotSupported, 'Daemonizing not supported on Windows' if Thin.win?
0
       raise ArgumentError, 'You must specify a pid_file to deamonize' unless @pid_file
0
       
0
       pwd = Dir.pwd # Current directory is changed during daemonization, so store it
0
@@ -50,7 +50,6 @@
0
     # Change privileges of the process
0
     # to the specified user and group.
0
     def change_privilege(user, group=user)
0
- check_plateform_support
0
       log ">> Changing process privilege to #{user}:#{group}"
0
       
0
       uid, gid = Process.euid, Process.egid
0
@@ -94,10 +93,6 @@
0
     end
0
     
0
     private
0
- def check_plateform_support
0
- raise RuntimeError, 'Daemonizing not supported on Windows' if RUBY_PLATFORM =~ /mswin/
0
- end
0
-
0
       def remove_pid_file
0
         File.delete(@pid_file) if @pid_file && File.exists?(@pid_file)
0
       end
...
38
39
40
41
42
43
 
 
 
 
 
44
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
45
46
47
...
38
39
40
 
 
 
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
0
@@ -38,10 +38,27 @@
0
       "HTTP/1.1 #{@status} #{HTTP_STATUS_CODES[@status.to_i]}\r\n#{headers_output}\r\n"
0
     end
0
     
0
- def headers=(key_value_pairs)
0
- key_value_pairs.each do |k, vs|
0
- vs.each_line { |v| @headers[k] = v.chomp }
0
+ if Thin.ruby_18?
0
+ def headers=(key_value_pairs)
0
+ key_value_pairs.each do |k, vs|
0
+ vs.each { |v| @headers[k] = v.chomp }
0
+ end
0
       end
0
+ else
0
+ # Ruby 1.9 doesn't have a String#each anymore.
0
+ # Rack spec doesn't take care of that yet, for now we just use
0
+ # +each+ but fallback to +each_line+ on strings.
0
+ # I wish we could remove that condition.
0
+ # To be reviewed when a new Rack spec comes out.
0
+ def headers=(key_value_pairs)
0
+ key_value_pairs.each do |k, vs|
0
+ if vs.is_a?(String)
0
+ vs.each_line { |v| @headers[k] = v.chomp }
0
+ else
0
+ vs.each { |v| @headers[k] = v.chomp }
0
+ end
0
+ end
0
+ end
0
     end
0
     
0
     # Close any resource used by the response
...
59
60
61
62
 
63
64
65
 
 
66
67
68
...
82
83
84
85
 
 
86
87
88
...
107
108
109
 
 
110
111
112
...
116
117
118
 
 
 
 
119
120
121
...
59
60
61
 
62
63
64
65
66
67
68
69
70
...
84
85
86
 
87
88
89
90
91
...
110
111
112
113
114
115
116
117
...
121
122
123
124
125
126
127
128
129
130
0
@@ -59,10 +59,12 @@
0
     
0
     # Start the server and listen for connections
0
     def start
0
- raise ArgumentError, "app required" unless @app
0
+ raise ArgumentError, 'app required' unless @app
0
       
0
       trap('INT') { stop }
0
       trap('TERM') { stop! }
0
+
0
+ at_exit { remove_socket_file } if @socket
0
             
0
       # See http://rubyeventmachine.com/pub/rdoc/files/EPOLL.html
0
       EventMachine.epoll
0
@@ -82,7 +84,8 @@
0
     
0
     # Stops the server by stopping the listening loop.
0
     def stop
0
- EventMachine.stop_event_loop
0
+ EventMachine.stop
0
+ remove_socket_file
0
     rescue
0
       warn "Error stopping : #{$!}"
0
     end
0
@@ -107,6 +110,8 @@
0
       end
0
       
0
       def start_server_on_socket
0
+ raise PlatformNotSupported, 'UNIX sockets not available on Windows' if Thin.win?
0
+
0
         log ">> Listening on #{@socket}, CTRL+C to stop"
0
         EventMachine.start_unix_domain_server(@socket, Connection, &method(:initialize_connection))
0
       end
0
@@ -116,6 +121,10 @@
0
         connection.app = @app
0
         connection.silent = @silent
0
         connection.unix_socket = !@socket.nil?
0
+ end
0
+
0
+ def remove_socket_file
0
+ File.delete(@socket) if @socket && File.exist?(@socket)
0
       end
0
   end
0
 end
...
 
 
1
2
3
4
 
 
5
6
7
 
 
8
9
10
11
12
...
21
22
23
24
 
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
 
52
53
54
55
...
56
57
58
59
 
60
61
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
62
63
64
...
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
...
27
28
29
 
30
31
32
33
34
35
 
36
37
38
39
40
41
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
42
43
44
45
46
...
47
48
49
 
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
0
@@ -1,10 +1,16 @@
0
+require 'erb'
0
+
0
 module Thin
0
   # Rack adapter to log stats to a Rack application
0
   module Stats
0
     class Adapter
0
+ include ERB::Util
0
+
0
       def initialize(app, path='/stats')
0
         @app = app
0
         @path = path
0
+
0
+ @template = ERB.new(TEMPLATE)
0
         
0
         @requests = 0
0
         @requests_finished = 0
0
0
0
@@ -21,34 +27,19 @@
0
       
0
       def log(env)
0
         @requests += 1
0
- @server = env['SERVER_SOFTWARE']
0
+ @last_request = Rack::Request.new(env)
0
         request_started_at = Time.now
0
         
0
         response = yield
0
         
0
         @requests_finished += 1
0
- @last_request_path = env['PATH_INFO']
0
         @last_request_time = Time.now - request_started_at
0
         
0
         response
0
       end
0
       
0
       def serve(env)
0
- body = '<html><body>'
0
- body << '<h1>Server stats</h1>'
0
- body << '<ul>'
0
- body << "<li>#{@requests} requests</li>"
0
- body << "<li>#{@requests_finished} requests finished</li>"
0
- body << "<li>#{@requests - @requests_finished} errors</li>"
0
- body << "<li>#{Time.now - @start_time} uptime</li>"
0
- body << "<li>Running on #{@server}</li>"
0
- body << '</ul>'
0
- body << '<h2>Last request</h2>'
0
- body << '<ul>'
0
- body << "<li>#{@last_request_path}</li>"
0
- body << "<li>Took #{@last_request_time} sec</li>"
0
- body << '</ul>'
0
- body << '</body></html>'
0
+ body = @template.result(binding)
0
         
0
         [
0
           200,
0
0
@@ -56,9 +47,257 @@
0
             'Content-Type' => 'text/html',
0
             'Content-Length' => body.size.to_s
0
           },
0
- body
0
+ [body]
0
         ]
0
       end
0
+
0
+ # Taken from Rails
0
+ def distance_of_time_in_words(from_time, to_time = 0, include_seconds = false)
0
+ from_time = from_time.to_time if from_time.respond_to?(:to_time)
0
+ to_time = to_time.to_time if to_time.respond_to?(:to_time)
0
+ distance_in_minutes = (((to_time - from_time).abs)/60).round
0
+ distance_in_seconds = ((to_time - from_time).abs).round
0
+
0
+ case distance_in_minutes
0
+ when 0..1
0
+ return (distance_in_minutes == 0) ? 'less than a minute' : '1 minute' unless include_seconds
0
+ case distance_in_seconds
0
+ when 0..4 then 'less than 5 seconds'
0
+ when 5..9 then 'less than 10 seconds'
0
+ when 10..19 then 'less than 20 seconds'
0
+ when 20..39 then 'half a minute'
0
+ when 40..59 then 'less than a minute'
0
+ else '1 minute'
0
+ end
0
+
0
+ when 2..44 then "#{distance_in_minutes} minutes"
0
+ when 45..89 then 'about 1 hour'
0
+ when 90..1439 then "about #{(distance_in_minutes.to_f / 60.0).round} hours"
0
+ when 1440..2879 then '1 day'
0
+ when 2880..43199 then "#{(distance_in_minutes / 1440).round} days"
0
+ when 43200..86399 then 'about 1 month'
0
+ when 86400..525599 then "#{(distance_in_minutes / 43200).round} months"
0
+ when 525600..1051199 then 'about 1 year'
0
+ else "over #{(distance_in_minutes / 525600).round} years"
0
+ end
0
+ end
0
+
0
+# Taken from Rack::ShowException
0
+# adapted from Django <djangoproject.com>
0
+# Copyright (c) 2005, the Lawrence Journal-World
0
+# Used under the modified BSD license:
0
+# http://www.xfree86.org/3.3.6/COPYRIGHT2.html#5
0
+TEMPLATE = <<'HTML'
0
+<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN" "http://www.w3.org/TR/html4/loose.dtd">
0
+<html lang="en">
0
+<head>
0
+ <meta http-equiv="content-type" content="text/html; charset=utf-8" />
0
+ <meta name="robots" content="NONE,NOARCHIVE" />
0
+ <title>Thin Stats</title>
0
+ <style type="text/css">
0
+ html * { padding:0; margin:0; }
0
+ body * { padding:10px 20px; }
0
+ body * * { padding:0; }
0
+ body { font:small sans-serif; }
0
+ body>div { border-bottom:1px solid #ddd; }
0
+ h1 { font-weight:normal; }
0
+ h2 { margin-bottom:.8em; }
0
+ h2 span { font-size:80%; color:#666; font-weight:normal; }
0
+ h3 { margin:1em 0 .5em 0; }
0
+ h4 { margin:0 0 .5em 0; font-weight: normal; }
0
+ table {
0
+ border:1px solid #ccc; border-collapse: collapse; background:white; }
0
+ tbody td, tbody th { vertical-align:top; padding:2px 3px; }
0
+ thead th {
0
+ padding:1px 6px 1px 3px; background:#fefefe; text-align:left;
0
+ font-weight:normal; font-size:11px; border:1px solid #ddd; }
0
+ tbody th { text-align:right; color:#666; padding-right:.5em; }
0
+ table.vars { margin:5px 0 2px 40px; }
0
+ table.vars td, table.req td { font-family:monospace; }
0
+ table td.code { width:100%;}
0
+ table td.code div { overflow:hidden; }
0
+ table.source th { color:#666; }
0
+ table.source td {
0
+ font-family:monospace; white-space:pre; border-bottom:1px solid #eee; }
0
+ ul.traceback { list-style-type:none; }
0
+ ul.traceback li.frame { margin-bottom:1em; }
0
+ div.context { margin: 10px 0; }
0
+ div.context ol {
0
+ padding-left:30px; margin:0 10px; list-style-position: inside; }
0
+ div.context ol li {
0
+ font-family:monospace; white-space:pre; color:#666; cursor:pointer; }
0
+ div.context ol.context-line li { color:black; background-color:#ccc; }
0
+ div.context ol.context-line li span { float: right; }
0
+ div.commands { margin-left: 40px; }
0
+ div.commands a { color:black; text-decoration:none; }
0
+ #summary { background: #ffc; }
0
+ #summary h2 { font-weight: normal; color: #666; }
0
+ #summary ul#quicklinks { list-style-type: none; margin-bottom: 2em; }
0
+ #summary ul#quicklinks li { float: left; padding: 0 1em; }