wvanbergen-request-log-analyzer 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,83 @@
1
+ require 'date'
2
+
3
+ module RailsAnalyzer
4
+ # Parse a rails log file
5
+ class LogParser
6
+
7
+ LOG_LINES = {
8
+ # Processing EmployeeController#index (for 123.123.123.123 at 2008-07-13 06:00:00) [GET]
9
+ :started => {
10
+ :teaser => /Processing/,
11
+ :regexp => /Processing (\w+)#(\w+) \(for (\d+\.\d+\.\d+\.\d+) at (\d\d\d\d-\d\d-\d\d \d\d:\d\d:\d\d)\) \[([A-Z]+)\]/,
12
+ :params => { :controller => 1, :action => 2, :ip => 3, :timestamp => 4, :method => 5}
13
+ },
14
+ # RuntimeError (Cannot destroy employee): /app/models/employee.rb:198:in `before_destroy'
15
+ :failed => {
16
+ :teaser => /Error/,
17
+ :regexp => /(\w+)(Error|Invalid) \((.*)\)\:(.*)/,
18
+ :params => { :error => 1, :exception_string => 3, :stack_trace => 4 }
19
+ },
20
+ # Completed in 0.21665 (4 reqs/sec) | Rendering: 0.00926 (4%) | DB: 0.00000 (0%) | 200 OK [http://demo.nu/employees]
21
+ :completed => {
22
+ :teaser => /Completed/,
23
+ :regexp => /Completed in (\d+\.\d{5}) \(\d+ reqs\/sec\) (\| Rendering: (\d+\.\d{5}) \(\d+\%\) )?(\| DB: (\d+\.\d{5}) \(\d+\%\) )?\| (\d\d\d).+\[(http.+)\]/,
24
+ :params => { :url => 7, :status => [6, :to_i], :duration => [1, :to_f], :rendering => [3, :to_f], :db => [5, :to_f] }
25
+ }
26
+ }
27
+
28
+ # LogParser initializer
29
+ # <tt>file</tt> The fileobject this LogParser wil operate on.
30
+ def initialize(file, options = {})
31
+ @file_name = file
32
+ @options = options
33
+ @file_size = File.size(@file_name)
34
+ end
35
+
36
+ def progress(&block)
37
+ @progress_handler = block
38
+ end
39
+
40
+ # Output a warning
41
+ # <tt>message</tt> The warning message (object)
42
+ def warn(message)
43
+ puts " -> " + message.to_s
44
+ end
45
+
46
+ # Finds a log line and then parses the information in the line.
47
+ # Yields a hash containing the information found.
48
+ # <tt>*line_types</tt> The log line types to look for (defaults to LOG_LINES.keys).
49
+ # Yeilds a Hash when it encounters a chunk of information.
50
+ def each(*line_types, &block)
51
+ # parse everything by default
52
+ line_types = LOG_LINES.keys if line_types.empty?
53
+
54
+ File.open(@file_name) do |file|
55
+
56
+ file.each_line do |line|
57
+
58
+ @progress_handler.call(file.pos, @file_size) if @progress_handler
59
+
60
+ line_types.each do |line_type|
61
+ if LOG_LINES[line_type][:teaser] =~ line
62
+ if LOG_LINES[line_type][:regexp] =~ line
63
+ request = { :type => line_type, :line => file.lineno }
64
+ LOG_LINES[line_type][:params].each do |key, value|
65
+ request[key] = case value
66
+ when Numeric; $~[value]
67
+ when Array; $~[value.first].send(value.last)
68
+ else; nil
69
+ end
70
+
71
+ end
72
+ yield(request) if block_given?
73
+ else
74
+ warn("Unparsable #{line_type} line: " + line[0..79]) unless line_type == :failed
75
+ end
76
+ end
77
+ end
78
+ end
79
+ @progress_handler.call(:finished, @file_size) if @progress_handler
80
+ end
81
+ end
82
+ end
83
+ end
@@ -0,0 +1,161 @@
1
+ require 'rubygems'
2
+ require 'sqlite3'
3
+
4
+ module RailsAnalyzer
5
+
6
+ # Set of functions that can be used to easily log requests into a SQLite3 Database.
7
+ class RecordInserter
8
+
9
+ attr_reader :database
10
+ attr_reader :current_request
11
+ attr_reader :warning_count
12
+
13
+ # Initializer
14
+ # <tt>db_file</tt> The file which will be used for the SQLite3 Database storage.
15
+ def initialize(db_file)
16
+ @database = SQLite3::Database.new(db_file)
17
+ @insert_statements = nil
18
+ @warning_count = 0
19
+ create_tables_if_needed!
20
+ end
21
+
22
+ # Calculate the database durations of the requests currenty in the database.
23
+ # Used if a logfile does contain any database durations.
24
+ def calculate_db_durations!
25
+ @database.execute('UPDATE "completed_queries" SET "database" = "duration" - "rendering" WHERE "database" IS NULL OR "database" = 0.0')
26
+ end
27
+
28
+ # Insert a batch of loglines into the database.
29
+ # Function prepares insert statements, yeilds and then closes and commits.
30
+ def insert_batch(&block)
31
+ @database.transaction
32
+ prepare_statements!
33
+ block.call(self)
34
+ close_prepared_statements!
35
+ @database.commit
36
+ rescue Exception => e
37
+ puts e.message
38
+ @database.rollback
39
+ end
40
+
41
+ def insert_warning(line, warning)
42
+ @database.execute("INSERT INTO parse_warnings (line, warning) VALUES (:line, :warning)", :line => line, :warning => warning)
43
+ @warning_count += 1
44
+ end
45
+
46
+ # Insert a request into the database.
47
+ # <tt>request</tt> The request to insert.
48
+ # <tt>close_statements</tt> Close prepared statements (default false)
49
+ def insert(request, close_statements = false)
50
+ unless @insert_statements
51
+ prepare_statements!
52
+ close_statements = true
53
+ end
54
+
55
+ if request[:type] && @insert_statements.has_key?(request[:type])
56
+ if request[:type] == :started
57
+ insert_warning(request[:line], "Unclosed request encountered on line #{request[:line]} (request started on line #{@current_request})") unless @current_request.nil?
58
+ @current_request = request[:line]
59
+ elsif [:failed, :completed].include?(request[:type])
60
+ @current_request = nil
61
+ end
62
+
63
+ begin
64
+ @insert_statements[request.delete(:type)].execute(request)
65
+ rescue SQLite3::Exception => e
66
+ insert_warning(request[:line], "Could not save log line to database: " + e.message.to_s)
67
+ end
68
+ else
69
+ insert_warning(request[:line], "Ignored unknown statement type")
70
+ end
71
+
72
+ close_prepared_statements! if close_statements
73
+ end
74
+
75
+ # Insert a batch of files into the database.
76
+ # <tt>db_file</tt> The filename of the database file to use.
77
+ # Returns the created database.
78
+ def self.insert_batch_into(db_file, options = {}, &block)
79
+ db = RecordInserter.new(db_file)
80
+ db.insert_batch(&block)
81
+ return db
82
+ end
83
+
84
+ def count(type)
85
+ @database.get_first_value("SELECT COUNT(*) FROM \"#{type}_requests\"").to_i
86
+ end
87
+
88
+ protected
89
+
90
+ # Prepare insert statements.
91
+ def prepare_statements!
92
+ @insert_statements = {
93
+ :started => @database.prepare("
94
+ INSERT INTO started_requests ( line, timestamp, ip, method, controller, action)
95
+ VALUES (:line, :timestamp, :ip, :method, :controller, :action)"),
96
+
97
+ :failed => @database.prepare("
98
+ INSERT INTO failed_requests ( line, exception_string, stack_trace, error)
99
+ VALUES (:line, :exception_string, :stack_trace, :error)"),
100
+
101
+ :completed => @database.prepare("
102
+ INSERT INTO completed_requests ( line, url, status, duration, rendering_time, database_time)
103
+ VALUES (:line, :url, :status, :duration, :rendering, :db)")
104
+ }
105
+ end
106
+
107
+ # Close all prepared statments
108
+ def close_prepared_statements!
109
+ @insert_statements.each { |key, stmt| stmt.close }
110
+ end
111
+
112
+ # Create the needed database tables if they don't exist.
113
+ def create_tables_if_needed!
114
+
115
+ @database.execute("
116
+ CREATE TABLE IF NOT EXISTS started_requests (
117
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
118
+ line INTEGER NOT NULL,
119
+ timestamp DATETIME NOT NULL,
120
+ controller VARCHAR(255) NOT NULL,
121
+ action VARCHAR(255) NOT NULL,
122
+ method VARCHAR(6) NOT NULL,
123
+ ip VARCHAR(6) NOT NULL
124
+ )
125
+ ");
126
+
127
+ @database.execute("
128
+ CREATE TABLE IF NOT EXISTS failed_requests (
129
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
130
+ line INTEGER NOT NULL,
131
+ started_request_id INTEGER,
132
+ error VARCHAR(255),
133
+ exception_string VARCHAR(255),
134
+ stack_trace TEXT
135
+ )
136
+ ");
137
+
138
+ @database.execute("
139
+ CREATE TABLE IF NOT EXISTS completed_requests (
140
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
141
+ line INTEGER NOT NULL,
142
+ started_request_id INTEGER,
143
+ url VARCHAR(255) NOT NULL,
144
+ hashed_url VARCHAR(255),
145
+ status INTEGER NOT NULL,
146
+ duration FLOAT,
147
+ rendering_time FLOAT,
148
+ database_time FLOAT
149
+ )
150
+ ");
151
+
152
+ @database.execute("CREATE TABLE IF NOT EXISTS parse_warnings (
153
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
154
+ line INTEGER NOT NULL,
155
+ warning VARCHAR(255) NOT NULL
156
+ )
157
+ ");
158
+ end
159
+
160
+ end
161
+ end
@@ -0,0 +1,121 @@
1
+ module RailsAnalyzer
2
+
3
+ # Functions to summarize an array of requets.
4
+ # Can calculate request counts, duratations, mean times etc. of all the requests given.
5
+ class Summarizer
6
+
7
+ attr_reader :actions
8
+ attr_reader :errors
9
+ attr_reader :request_count
10
+ attr_reader :request_time_graph
11
+ attr_reader :first_request_at
12
+ attr_reader :last_request_at
13
+
14
+ attr_accessor :blocker_duration
15
+ DEFAULT_BLOCKER_DURATION = 1.0
16
+
17
+ # Initializer. Sets global variables
18
+ # Options
19
+ # * <tt>:calculate_database</tt> Calculate the database times if they are not explicitly logged.
20
+ def initialize(options = {})
21
+ @actions = {}
22
+ @blockers = {}
23
+ @errors = {}
24
+ @request_count = 0
25
+ @blocker_duration = DEFAULT_BLOCKER_DURATION
26
+ @calculate_database = options[:calculate_database]
27
+ @request_time_graph = [0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0]
28
+ end
29
+
30
+ # Check if any of the request parsed had a timestamp.
31
+ def has_timestamps?
32
+ @first_request_at
33
+ end
34
+
35
+ # Parse a request string into a hash containing all keys found in the string.
36
+ # Yields the hash found to the block operator.
37
+ # <tt>request</tt> The request string to parse.
38
+ # <tt>&block</tt> Block operator
39
+ def group(request, &block)
40
+ request[:duration] ||= 0
41
+
42
+ case request[:type]
43
+ when :started
44
+ @first_request_at ||= request[:timestamp] # assume time-based order of file
45
+ @last_request_at = request[:timestamp] # assume time-based order of file
46
+ @request_time_graph[request[:timestamp][11..12].to_i] +=1
47
+
48
+ when :completed
49
+ @request_count += 1
50
+ hash = block_given? ? yield(request) : request.hash
51
+
52
+ @actions[hash] ||= {:count => 0, :total_time => 0.0, :total_db_time => 0.0, :total_rendering_time => 0.0,
53
+ :min_time => request[:duration], :max_time => request[:duration] }
54
+
55
+ @actions[hash][:count] += 1
56
+ @actions[hash][:total_time] += request[:duration]
57
+ @actions[hash][:total_db_time] += request[:db] if request[:db]
58
+ @actions[hash][:total_db_time] += request[:duration] - request[:rendering] if @calculate_database
59
+
60
+ @actions[hash][:total_rendering_time] += request[:rendering] if request[:rendering]
61
+
62
+ @actions[hash][:min_time] = [@actions[hash][:min_time], request[:duration]].min
63
+ @actions[hash][:max_time] = [@actions[hash][:min_time], request[:duration]].max
64
+ @actions[hash][:mean_time] = @actions[hash][:total_time] / @actions[hash][:count].to_f
65
+
66
+ @actions[hash][:mean_db_time] = @actions[hash][:total_db_time] / @actions[hash][:count].to_f
67
+ @actions[hash][:mean_rendering_time] = @actions[hash][:total_rendering_time] / @actions[hash][:count].to_f
68
+
69
+ if request[:duration] > @blocker_duration
70
+ @blockers[hash] ||= { :count => 0, :total_time => 0.0 }
71
+ @blockers[hash][:count] += 1
72
+ @blockers[hash][:total_time] += request[:duration]
73
+ end
74
+
75
+ when :failed
76
+ hash = request[:error]
77
+ @errors[hash] ||= {:count => 0, :exception_strings => {}}
78
+ @errors[hash][:count] +=1
79
+
80
+ @errors[hash][:exception_strings][request[:exception_string]] ||= 0
81
+ @errors[hash][:exception_strings][request[:exception_string]] += 1
82
+ end
83
+ end
84
+
85
+ # Return a list of requests sorted on a specific action field
86
+ # <tt>field</tt> The action field to sort by.
87
+ # <tt>min_count</tt> Values which fall below this amount are not returned (default nil).
88
+ def sort_actions_by(field, min_count = nil)
89
+ actions = min_count.nil? ? @actions.to_a : @actions.delete_if { |k, v| v[:count] < min_count}.to_a
90
+ actions.sort { |a, b| (a[1][field.to_sym] <=> b[1][field.to_sym]) }
91
+ end
92
+
93
+ # Returns a list of request blockers sorted by a specific field
94
+ # <tt>field</tt> The action field to sort by.
95
+ # <tt>min_count</tt> Values which fall below this amount are not returned (default @blocker_duration).
96
+ def sort_blockers_by(field, min_count = @blocker_duration)
97
+ blockers = min_count.nil? ? @blockers.to_a : @blockers.delete_if { |k, v| v[:count] < min_count}.to_a
98
+ blockers.sort { |a, b| a[1][field.to_sym] <=> b[1][field.to_sym] }
99
+ end
100
+
101
+ # Returns a list of request blockers sorted by a specific field
102
+ # <tt>field</tt> The action field to sort by.
103
+ # <tt>min_count</tt> Values which fall below this amount are not returned (default @blocker_duration).
104
+ def sort_errors_by(field, min_count = nil)
105
+ errors = min_count.nil? ? @errors.to_a : @errors.delete_if { |k, v| v[:count] < min_count}.to_a
106
+ errors.sort { |a, b| a[1][field.to_sym] <=> b[1][field.to_sym] }
107
+ end
108
+
109
+ # Calculate the duration of a request
110
+ # Returns a DateTime object if possible, 0 otherwise.
111
+ def duration
112
+ (@last_request_at && @first_request_at) ? (DateTime.parse(@last_request_at) - DateTime.parse(@first_request_at)).round : 0
113
+ end
114
+
115
+ # Check if the request time graph usable data.
116
+ def request_time_graph?
117
+ @request_time_graph.uniq != [0] && duration > 0
118
+ end
119
+
120
+ end
121
+ end
@@ -0,0 +1,103 @@
1
+ =begin
2
+ index:Ej
3
+
4
+ = Ruby/ProgressBar: A Text Progress Bar Library for Ruby
5
+
6
+ Last Modified: 2005-05-22 00:28:04
7
+
8
+ --
9
+
10
+ Ruby/ProgressBar is a text progress bar library for Ruby.
11
+ It can indicate progress with percentage, a progress bar,
12
+ and estimated remaining time.
13
+
14
+ The latest version of Ruby/ProgressBar is available at
15
+ ((<URL:http://namazu.org/~satoru/ruby-progressbar/>))
16
+ .
17
+
18
+ == Examples
19
+
20
+ % irb --simple-prompt -r progressbar
21
+ >> pbar = ProgressBar.new("test", 100)
22
+ => (ProgressBar: 0/100)
23
+ >> 100.times {sleep(0.1); pbar.inc}; pbar.finish
24
+ test: 100% |oooooooooooooooooooooooooooooooooooooooo| Time: 00:00:10
25
+ => nil
26
+
27
+ >> pbar = ProgressBar.new("test", 100)
28
+ => (ProgressBar: 0/100)
29
+ >> (1..100).each{|x| sleep(0.1); pbar.set(x)}; pbar.finish
30
+ test: 67% |oooooooooooooooooooooooooo | ETA: 00:00:03
31
+
32
+ == API
33
+
34
+ --- ProgressBar#new (title, total, out = STDERR)
35
+ Display the initial progress bar and return a
36
+ ProgressBar object. ((|title|)) specifies the title,
37
+ and ((|total|)) specifies the total cost of processing.
38
+ Optional parameter ((|out|)) specifies the output IO.
39
+
40
+ The display of the progress bar is updated when one or
41
+ more percent is proceeded or one or more seconds are
42
+ elapsed from the previous display.
43
+
44
+ --- ProgressBar#inc (step = 1)
45
+ Increase the internal counter by ((|step|)) and update
46
+ the display of the progress bar. Display the estimated
47
+ remaining time on the right side of the bar. The counter
48
+ does not go beyond the ((|total|)).
49
+
50
+ --- ProgressBar#set (count)
51
+ Set the internal counter to ((|count|)) and update the
52
+ display of the progress bar. Display the estimated
53
+ remaining time on the right side of the bar. Raise if
54
+ ((|count|)) is a negative number or a number more than
55
+ the ((|total|)).
56
+
57
+ --- ProgressBar#finish
58
+ Stop the progress bar and update the display of progress
59
+ bar. Display the elapsed time on the right side of the bar.
60
+ The progress bar always stops at 100 % by the method.
61
+
62
+ --- ProgressBar#halt
63
+ Stop the progress bar and update the display of progress
64
+ bar. Display the elapsed time on the right side of the bar.
65
+ The progress bar stops at the current percentage by the method.
66
+
67
+ --- ProgressBar#format=
68
+ Set the format for displaying a progress bar.
69
+ Default: "%-14s %3d%% %s %s".
70
+
71
+ --- ProgressBar#format_arguments=
72
+ Set the methods for displaying a progress bar.
73
+ Default: [:title, :percentage, :bar, :stat].
74
+
75
+ --- ProgressBar#file_transfer_mode
76
+ Use :stat_for_file_transfer instead of :stat to display
77
+ transfered bytes and transfer rate.
78
+
79
+
80
+ ReverseProgressBar class is also available. The
81
+ functionality is identical to ProgressBar but the direction
82
+ of the progress bar is just opposite.
83
+
84
+ == Limitations
85
+
86
+ Since the progress is calculated by the proportion to the
87
+ total cost of processing, Ruby/ProgressBar cannot be used if
88
+ the total cost of processing is unknown in advance.
89
+ Moreover, the estimation of remaining time cannot be
90
+ accurately performed if the progress does not flow uniformly.
91
+
92
+ == Download
93
+
94
+ Ruby/ProgressBar is a free software with ABSOLUTELY NO WARRANTY
95
+ under the terms of Ruby's license.
96
+
97
+ * ((<URL:http://namazu.org/~satoru/ruby-progressbar/ruby-progressbar-0.9.tar.gz>))
98
+ * ((<URL:http://cvs.namazu.org/ruby-progressbar/>))
99
+
100
+ --
101
+
102
+ - ((<Satoru Takabayashi|URL:http://namazu.org/~satoru/>)) -
103
+ =end
@@ -0,0 +1,100 @@
1
+ =begin
2
+ index:eJ
3
+
4
+ = Ruby/ProgressBar: �ץ����쥹�С���ƥ����Ȥ�ɽ������ Ruby�ѤΥ饤�֥��
5
+
6
+ �ǽ�������: 2005-05-22 00:28:53
7
+
8
+
9
+ --
10
+
11
+ Ruby/ProgressBar �ϥץ����쥹�С���ƥ����Ȥ�ɽ������ Ruby��
12
+ �Υ饤�֥��Ǥ��������ο�Ľ������ѡ�����ȡ��ץ����쥹�С���
13
+ ����ӿ���Ĥ���֤Ȥ���ɽ�����ޤ���
14
+
15
+ �ǿ��Ǥ�
16
+ ((<URL:http://namazu.org/~satoru/ruby-progressbar/>))
17
+ ���������ǽ�Ǥ�
18
+
19
+ == ������
20
+
21
+ % irb --simple-prompt -r progressbar
22
+ >> pbar = ProgressBar.new("test", 100)
23
+ => (ProgressBar: 0/100)
24
+ >> 100.times {sleep(0.1); pbar.inc}; pbar.finish
25
+ test: 100% |oooooooooooooooooooooooooooooooooooooooo| Time: 00:00:10
26
+ => nil
27
+
28
+ >> pbar = ProgressBar.new("test", 100)
29
+ => (ProgressBar: 0/100)
30
+ >> (1..100).each{|x| sleep(0.1); pbar.set(x)}; pbar.finish
31
+ test: 67% |oooooooooooooooooooooooooo | ETA: 00:00:03
32
+
33
+ == API
34
+
35
+ --- ProgressBar#new (title, total, out = STDERR)
36
+ �ץ����쥹�С��ν�����֤�ɽ������������ ProgressBar����
37
+ �������Ȥ��֤���((|title|)) �Ǹ��Ф���((|total|)) �ǽ�
38
+ �������פ�((|out|)) �ǽ������ IO �����ꤹ�롣
39
+
40
+ �ץ����쥹�С���ɽ���ϡ������ɽ�������Ľ�� 1%�ʾ夢��
41
+ ���Ȥ������뤤�� 1�ðʾ�вᤷ�����˹�������ޤ���
42
+
43
+ --- ProgressBar#inc (step = 1)
44
+ �����Υ����󥿤� ((|step|)) �������ʤ�ơ��ץ����쥹�С�
45
+ ��ɽ���򹹿����롣�С��α�¦�ˤϿ���Ĥ���֤�ɽ�����롣
46
+ �����󥿤� ((|total|)) ��ۤ��ƿʤळ�ȤϤʤ���
47
+
48
+ --- ProgressBar#set (count)
49
+ �����󥿤��ͤ� ((|count|)) �����ꤷ���ץ����쥹�С���
50
+ ɽ���򹹿����롣�С��α�¦�ˤϿ���Ĥ���֤�ɽ�����롣
51
+ ((|count|)) �˥ޥ��ʥ����ͤ��뤤�� ((|total|)) ����礭
52
+ ���ͤ��Ϥ����㳰��ȯ�����롣
53
+
54
+ --- ProgressBar#finish
55
+ �ץ����쥹�С�����ߤ����ץ����쥹�С���ɽ���򹹿����롣
56
+ �ץ����쥹�С��α�¦�ˤϷв���֤�ɽ�����롣
57
+ ���ΤȤ����ץ����쥹�С��� 100% �ǽ�λ���롣
58
+
59
+ --- ProgressBar#halt
60
+ �ץ����쥹�С�����ߤ����ץ����쥹�С���ɽ���򹹿����롣
61
+ �ץ����쥹�С��α�¦�ˤϷв���֤�ɽ�����롣
62
+ ���ΤȤ����ץ����쥹�С��Ϥ��λ����Υѡ�����ơ����ǽ�λ���롣
63
+
64
+ --- ProgressBar#format=
65
+ �ץ����쥹�С�ɽ���Υե����ޥåȤ����ꤹ�롣
66
+ ̤�ѹ����� "%-14s %3d%% %s %s"
67
+
68
+ --- ProgressBar#format_arguments=
69
+ �ץ����쥹�С�ɽ���˻Ȥ��ؿ������ꤹ�롣
70
+ ̤�ѹ����� [:title, :percentage, :bar, :stat]
71
+ �ե�����ž�����ˤ� :stat ���Ѥ��� :stat_for_file_transfer
72
+ ��Ȥ���ž���Х��ȿ���ž��®�٤�ɽ���Ǥ��롣
73
+
74
+ --- ProgressBar#file_transfer_mode
75
+ �ץ����쥹�С�ɽ���� :stat ���Ѥ��� :stat_for_file_transfer
76
+ ��Ȥ���ž���Х��ȿ���ž��®�٤�ɽ�����롣
77
+
78
+
79
+ ReverseProgressBar �Ȥ������饹���󶡤���ޤ�����ǽ��
80
+ ProgressBar �Ȥޤä���Ʊ���Ǥ������ץ����쥹�С��οʹ�������
81
+ �դˤʤäƤ��ޤ���
82
+
83
+ == ���»���
84
+
85
+ ��Ľ��������������פ��Ф�����Ȥ��Ʒ׻����뤿�ᡢ��������
86
+ �פ������ˤ狼��ʤ������ǤϻȤ��ޤ��󡣤ޤ�����Ľ��ή�줬��
87
+ ��Ǥʤ��Ȥ��ˤϻĤ���֤ο�����������Ԥ��ޤ���
88
+
89
+ == �����������
90
+
91
+ Ruby �Υ饤���󥹤˽��ä��ե꡼���եȥ������Ȥ��Ƹ������ޤ���
92
+ ������̵�ݾڤǤ���
93
+
94
+ * ((<URL:http://namazu.org/~satoru/ruby-progressbar/ruby-progressbar-0.9.tar.gz>))
95
+ * ((<URL:http://cvs.namazu.org/ruby-progressbar/>))
96
+
97
+ --
98
+
99
+ - ((<Satoru Takabayashi|URL:http://namazu.org/~satoru/>)) -
100
+ =end