lackac-request-log-analyzer 0.1.3
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 +20 -0
- data/README +132 -0
- data/Rakefile +72 -0
- data/TODO +18 -0
- data/bin/request-log-analyzer +115 -0
- data/bin/request-log-database +81 -0
- data/lib/base/log_parser.rb +68 -0
- data/lib/base/record_inserter.rb +139 -0
- data/lib/base/summarizer.rb +71 -0
- data/lib/bashcolorizer.rb +60 -0
- data/lib/command_line/arguments.rb +129 -0
- data/lib/command_line/exceptions.rb +37 -0
- data/lib/command_line/flag.rb +51 -0
- data/lib/merb_analyzer/log_parser.rb +26 -0
- data/lib/merb_analyzer/summarizer.rb +61 -0
- data/lib/rails_analyzer/log_parser.rb +25 -0
- data/lib/rails_analyzer/record_inserter.rb +39 -0
- data/lib/rails_analyzer/summarizer.rb +67 -0
- data/lib/ruby-progressbar/progressbar.en.rd +103 -0
- data/lib/ruby-progressbar/progressbar.ja.rd +100 -0
- data/lib/ruby-progressbar/progressbar.rb +236 -0
- data/output/blockers.rb +11 -0
- data/output/errors.rb +9 -0
- data/output/hourly_spread.rb +28 -0
- data/output/mean_db_time.rb +7 -0
- data/output/mean_rendering_time.rb +7 -0
- data/output/mean_time.rb +7 -0
- data/output/most_requested.rb +6 -0
- data/output/timespan.rb +15 -0
- data/output/total_db_time.rb +6 -0
- data/output/total_time.rb +6 -0
- data/output/usage.rb +15 -0
- data/test/log_fragments/fragment_1.log +59 -0
- data/test/log_fragments/fragment_2.log +5 -0
- data/test/log_fragments/merb_1.log +84 -0
- data/test/merb_log_parser_test.rb +39 -0
- data/test/rails_log_parser_test.rb +86 -0
- data/test/record_inserter_test.rb +45 -0
- data/test/tasks.rake +8 -0
- metadata +105 -0
@@ -0,0 +1,139 @@
|
|
1
|
+
require 'rubygems'
|
2
|
+
require 'sqlite3'
|
3
|
+
|
4
|
+
module Base
|
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, options = {})
|
16
|
+
@database = SQLite3::Database.new(db_file)
|
17
|
+
@insert_statements = nil
|
18
|
+
@warning_count = 0
|
19
|
+
create_tables_if_needed!
|
20
|
+
|
21
|
+
self.initialize_hook(options) if self.respond_to?(:initialize_hook)
|
22
|
+
end
|
23
|
+
|
24
|
+
# Calculate the database durations of the requests currenty in the database.
|
25
|
+
# Used if a logfile does contain any database durations.
|
26
|
+
def calculate_db_durations!
|
27
|
+
@database.execute('UPDATE "completed_queries" SET "database" = "duration" - "rendering" WHERE "database" IS NULL OR "database" = 0.0')
|
28
|
+
end
|
29
|
+
|
30
|
+
# Insert a batch of loglines into the database.
|
31
|
+
# Function prepares insert statements, yeilds and then closes and commits.
|
32
|
+
def insert_batch(&block)
|
33
|
+
@database.transaction
|
34
|
+
prepare_statements!
|
35
|
+
block.call(self)
|
36
|
+
close_prepared_statements!
|
37
|
+
@database.commit
|
38
|
+
rescue Exception => e
|
39
|
+
puts e.message
|
40
|
+
@database.rollback
|
41
|
+
end
|
42
|
+
|
43
|
+
def insert_warning(line, warning)
|
44
|
+
@database.execute("INSERT INTO parse_warnings (line, warning) VALUES (:line, :warning)", :line => line, :warning => warning)
|
45
|
+
@warning_count += 1
|
46
|
+
end
|
47
|
+
|
48
|
+
# Insert a request into the database.
|
49
|
+
# def insert(request, close_statements = false)
|
50
|
+
# raise 'No insert defined for this log file type'
|
51
|
+
# end
|
52
|
+
|
53
|
+
# Insert a batch of files into the database.
|
54
|
+
# <tt>db_file</tt> The filename of the database file to use.
|
55
|
+
# Returns the created database.
|
56
|
+
def self.insert_batch_into(db_file, options = {}, &block)
|
57
|
+
db = self.new(db_file)
|
58
|
+
db.insert_batch(&block)
|
59
|
+
return db
|
60
|
+
end
|
61
|
+
|
62
|
+
def count(type)
|
63
|
+
@database.get_first_value("SELECT COUNT(*) FROM \"#{type}_requests\"").to_i
|
64
|
+
end
|
65
|
+
|
66
|
+
protected
|
67
|
+
|
68
|
+
# Prepare insert statements.
|
69
|
+
def prepare_statements!
|
70
|
+
@insert_statements = {
|
71
|
+
:started => @database.prepare("
|
72
|
+
INSERT INTO started_requests ( line, timestamp, ip, method, controller, action)
|
73
|
+
VALUES (:line, :timestamp, :ip, :method, :controller, :action)"),
|
74
|
+
|
75
|
+
:failed => @database.prepare("
|
76
|
+
INSERT INTO failed_requests ( line, exception_string, stack_trace, error)
|
77
|
+
VALUES (:line, :exception_string, :stack_trace, :error)"),
|
78
|
+
|
79
|
+
:completed => @database.prepare("
|
80
|
+
INSERT INTO completed_requests ( line, url, status, duration, rendering_time, database_time)
|
81
|
+
VALUES (:line, :url, :status, :duration, :rendering, :db)")
|
82
|
+
}
|
83
|
+
end
|
84
|
+
|
85
|
+
# Close all prepared statments
|
86
|
+
def close_prepared_statements!
|
87
|
+
@insert_statements.each { |key, stmt| stmt.close }
|
88
|
+
end
|
89
|
+
|
90
|
+
# Create the needed database tables if they don't exist.
|
91
|
+
def create_tables_if_needed!
|
92
|
+
|
93
|
+
@database.execute("
|
94
|
+
CREATE TABLE IF NOT EXISTS started_requests (
|
95
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
96
|
+
line INTEGER NOT NULL,
|
97
|
+
timestamp DATETIME NOT NULL,
|
98
|
+
controller VARCHAR(255) NOT NULL,
|
99
|
+
action VARCHAR(255) NOT NULL,
|
100
|
+
method VARCHAR(6) NOT NULL,
|
101
|
+
ip VARCHAR(6) NOT NULL
|
102
|
+
)
|
103
|
+
");
|
104
|
+
|
105
|
+
@database.execute("
|
106
|
+
CREATE TABLE IF NOT EXISTS failed_requests (
|
107
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
108
|
+
line INTEGER NOT NULL,
|
109
|
+
started_request_id INTEGER,
|
110
|
+
error VARCHAR(255),
|
111
|
+
exception_string VARCHAR(255),
|
112
|
+
stack_trace TEXT
|
113
|
+
)
|
114
|
+
");
|
115
|
+
|
116
|
+
@database.execute("
|
117
|
+
CREATE TABLE IF NOT EXISTS completed_requests (
|
118
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
119
|
+
line INTEGER NOT NULL,
|
120
|
+
started_request_id INTEGER,
|
121
|
+
url VARCHAR(255) NOT NULL,
|
122
|
+
hashed_url VARCHAR(255),
|
123
|
+
status INTEGER NOT NULL,
|
124
|
+
duration FLOAT,
|
125
|
+
rendering_time FLOAT,
|
126
|
+
database_time FLOAT
|
127
|
+
)
|
128
|
+
");
|
129
|
+
|
130
|
+
@database.execute("CREATE TABLE IF NOT EXISTS parse_warnings (
|
131
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
132
|
+
line INTEGER NOT NULL,
|
133
|
+
warning VARCHAR(255) NOT NULL
|
134
|
+
)
|
135
|
+
");
|
136
|
+
end
|
137
|
+
|
138
|
+
end
|
139
|
+
end
|
@@ -0,0 +1,71 @@
|
|
1
|
+
module Base
|
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
|
+
attr_reader :actions
|
7
|
+
attr_reader :errors
|
8
|
+
attr_reader :request_count
|
9
|
+
attr_reader :request_time_graph
|
10
|
+
attr_reader :first_request_at
|
11
|
+
attr_reader :last_request_at
|
12
|
+
attr_reader :methods
|
13
|
+
|
14
|
+
attr_accessor :blocker_duration
|
15
|
+
DEFAULT_BLOCKER_DURATION = 1.0
|
16
|
+
|
17
|
+
# Initializer. Sets global variables
|
18
|
+
# Options
|
19
|
+
def initialize(options = {})
|
20
|
+
@actions = {}
|
21
|
+
@blockers = {}
|
22
|
+
@errors = {}
|
23
|
+
@request_count = 0
|
24
|
+
@blocker_duration = DEFAULT_BLOCKER_DURATION
|
25
|
+
@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]
|
26
|
+
@methods = {:GET => 0, :POST => 0, :PUT => 0, :DELETE => 0}
|
27
|
+
|
28
|
+
self.initialize_hook(options) if self.respond_to?(:initialize_hook)
|
29
|
+
end
|
30
|
+
|
31
|
+
# Check if any of the request parsed had a timestamp.
|
32
|
+
def has_timestamps?
|
33
|
+
@first_request_at
|
34
|
+
end
|
35
|
+
|
36
|
+
# Calculate the duration of a request
|
37
|
+
# Returns a DateTime object if possible, 0 otherwise.
|
38
|
+
def duration
|
39
|
+
(@last_request_at && @first_request_at) ? (DateTime.parse(@last_request_at) - DateTime.parse(@first_request_at)).ceil : 0
|
40
|
+
end
|
41
|
+
|
42
|
+
# Check if the request time graph usable data.
|
43
|
+
def request_time_graph?
|
44
|
+
@request_time_graph.uniq != [0] && duration > 0
|
45
|
+
end
|
46
|
+
|
47
|
+
# Return a list of requests sorted on a specific action field
|
48
|
+
# <tt>field</tt> The action field to sort by.
|
49
|
+
# <tt>min_count</tt> Values which fall below this amount are not returned (default nil).
|
50
|
+
def sort_actions_by(field, min_count = nil)
|
51
|
+
actions = min_count.nil? ? @actions.to_a : @actions.delete_if { |k, v| v[:count] < min_count}.to_a
|
52
|
+
actions.sort { |a, b| (a[1][field.to_sym] <=> b[1][field.to_sym]) }
|
53
|
+
end
|
54
|
+
|
55
|
+
# Returns a list of request blockers sorted by a specific field
|
56
|
+
# <tt>field</tt> The action field to sort by.
|
57
|
+
# <tt>min_count</tt> Values which fall below this amount are not returned (default @blocker_duration).
|
58
|
+
def sort_blockers_by(field, min_count = @blocker_duration)
|
59
|
+
blockers = min_count.nil? ? @blockers.to_a : @blockers.delete_if { |k, v| v[:count] < min_count}.to_a
|
60
|
+
blockers.sort { |a, b| a[1][field.to_sym] <=> b[1][field.to_sym] }
|
61
|
+
end
|
62
|
+
|
63
|
+
# Returns a list of request blockers sorted by a specific field
|
64
|
+
# <tt>field</tt> The action field to sort by.
|
65
|
+
# <tt>min_count</tt> Values which fall below this amount are not returned (default @blocker_duration).
|
66
|
+
def sort_errors_by(field, min_count = nil)
|
67
|
+
errors = min_count.nil? ? @errors.to_a : @errors.delete_if { |k, v| v[:count] < min_count}.to_a
|
68
|
+
errors.sort { |a, b| a[1][field.to_sym] <=> b[1][field.to_sym] }
|
69
|
+
end
|
70
|
+
end
|
71
|
+
end
|
@@ -0,0 +1,60 @@
|
|
1
|
+
# Colorize a text output with the given color if.
|
2
|
+
# <tt>text</tt> The text to colorize.
|
3
|
+
# <tt>color_code</tt> The color code string to set
|
4
|
+
# <tt>color</tt> Does not color if false. Defaults to ($arguments && $arguments[:colorize])
|
5
|
+
def colorize(text, color_code, color = $arguments && $arguments[:colorize])
|
6
|
+
color ? "#{color_code}#{text}\e[0m" : text
|
7
|
+
end
|
8
|
+
|
9
|
+
# Draw a red line of text
|
10
|
+
def red(text)
|
11
|
+
colorize(text, "\e[31m")
|
12
|
+
end
|
13
|
+
|
14
|
+
# Draw a Green line of text
|
15
|
+
def green(text)
|
16
|
+
colorize(text, "\e[32m")
|
17
|
+
end
|
18
|
+
|
19
|
+
# Draw a Yellow line of text
|
20
|
+
def yellow(text)
|
21
|
+
colorize(text, "\e[33m")
|
22
|
+
end
|
23
|
+
|
24
|
+
# Draw a Yellow line of text
|
25
|
+
def blue(text)
|
26
|
+
colorize(text, "\e[34m")
|
27
|
+
end
|
28
|
+
|
29
|
+
def white(text)
|
30
|
+
colorize(text, "\e[37m")
|
31
|
+
end
|
32
|
+
|
33
|
+
|
34
|
+
#STYLE = {
|
35
|
+
# :default => “33[0m”,
|
36
|
+
# # styles
|
37
|
+
# :bold => “33[1m”,
|
38
|
+
# :underline => “33[4m”,
|
39
|
+
# :blink => “33[5m”,
|
40
|
+
# :reverse => “33[7m”,
|
41
|
+
# :concealed => “33[8m”,
|
42
|
+
# # font colors
|
43
|
+
# :black => “33[30m”,
|
44
|
+
# :red => “33[31m”,
|
45
|
+
# :green => “33[32m”,
|
46
|
+
# :yellow => “33[33m”,
|
47
|
+
# :blue => “33[34m”,
|
48
|
+
# :magenta => “33[35m”,
|
49
|
+
# :cyan => “33[36m”,
|
50
|
+
# :white => “33[37m”,
|
51
|
+
# # background colors
|
52
|
+
# :on_black => “33[40m”,
|
53
|
+
# :on_red => “33[41m”,
|
54
|
+
# :on_green => “33[42m”,
|
55
|
+
# :on_yellow => “33[43m”,
|
56
|
+
# :on_blue => “33[44m”,
|
57
|
+
# :on_magenta => “33[45m”,
|
58
|
+
# :on_cyan => “33[46m”,
|
59
|
+
# :on_white => “33[47m” }
|
60
|
+
#
|
@@ -0,0 +1,129 @@
|
|
1
|
+
require "#{File.dirname(__FILE__)}/flag"
|
2
|
+
require "#{File.dirname(__FILE__)}/exceptions"
|
3
|
+
|
4
|
+
# Module used to parse commandline arguments
|
5
|
+
module CommandLine
|
6
|
+
|
7
|
+
# Parse argument lists and return an argument object containing all set flags and switches.
|
8
|
+
class Arguments
|
9
|
+
|
10
|
+
FLAG_REGEXP = /^--?[A-z0-9]/
|
11
|
+
|
12
|
+
attr_reader :flag_definitions
|
13
|
+
|
14
|
+
attr_reader :flags
|
15
|
+
attr_reader :files
|
16
|
+
attr_reader :command
|
17
|
+
|
18
|
+
attr_accessor :required_files
|
19
|
+
|
20
|
+
# Initializer.
|
21
|
+
# <tt>arguments</tt> The arguments which are going to be parsed (defaults to $*).
|
22
|
+
def initialize(arguments = $*, &block)
|
23
|
+
@arguments = arguments
|
24
|
+
@flag_definitions = {}
|
25
|
+
@begins_with_command = false
|
26
|
+
end
|
27
|
+
|
28
|
+
# Parse a list of arguments. Intatiates a Argument object with the given arguments and yeilds
|
29
|
+
# it so that flags and switches can be set by the application.
|
30
|
+
# <tt>arguments</tt> The arguments which are going to be parsed (defaults to $*).
|
31
|
+
# Returns the arguments object.parse!
|
32
|
+
def self.parse(arguments = $*, &block)
|
33
|
+
cla = Arguments.new(arguments)
|
34
|
+
yield(cla)
|
35
|
+
return cla.parse!
|
36
|
+
end
|
37
|
+
|
38
|
+
# Handle argument switches for the application
|
39
|
+
# <tt>switch</tt> A switch symbol like :fast
|
40
|
+
# <tt>switch_alias</tt> An short alias for the same switch (:f).
|
41
|
+
def switch(switch, switch_alias = nil)
|
42
|
+
return self.flag(switch, :alias => switch_alias, :expects => nil)
|
43
|
+
end
|
44
|
+
|
45
|
+
# Handle argument flags for the application
|
46
|
+
# <tt>flag</tt> A flag symbol like :fast
|
47
|
+
# Options
|
48
|
+
# * <tt>:expects</tt> Expects a value after the flag
|
49
|
+
def flag(flag, options)
|
50
|
+
options[:expects] = String unless options.has_key?(:expects)
|
51
|
+
argument = Flag.new(flag, options)
|
52
|
+
@flag_definitions[argument.to_argument] = argument
|
53
|
+
@flag_definitions[argument.to_alias] = argument if argument.has_alias?
|
54
|
+
return argument
|
55
|
+
end
|
56
|
+
|
57
|
+
# If called argument list must begin with a command.
|
58
|
+
# <tt>begins_w_command</tt> Defaults to true.
|
59
|
+
def begins_with_command!(begins_w_command=true)
|
60
|
+
@begins_with_command = begins_w_command
|
61
|
+
end
|
62
|
+
|
63
|
+
# Unknown flags will be silently ignored.
|
64
|
+
# <tt>ignore</tt> Defaults to true.
|
65
|
+
def ignore_unknown_flags!(ignore = true)
|
66
|
+
@ignore_unknown_flags = ignore
|
67
|
+
end
|
68
|
+
|
69
|
+
def [](name)
|
70
|
+
return flags[name.to_s.gsub(/_/, '-').to_sym]
|
71
|
+
end
|
72
|
+
|
73
|
+
# Parse the flags and switches set by the application.
|
74
|
+
# Returns an arguments object containing the flags and switches found in the commandline.
|
75
|
+
def parse!
|
76
|
+
@flags = {}
|
77
|
+
@files = []
|
78
|
+
|
79
|
+
i = 0
|
80
|
+
while @arguments.length > i do
|
81
|
+
arg = @arguments[i]
|
82
|
+
if FLAG_REGEXP =~ arg
|
83
|
+
if @flag_definitions.has_key?(arg)
|
84
|
+
flag = @flag_definitions[arg]
|
85
|
+
if flag.expects_argument?
|
86
|
+
|
87
|
+
if @arguments.length > (i + 1) && @arguments[i + 1]
|
88
|
+
@flags[flag.name] = @arguments[i + 1]
|
89
|
+
i += 1
|
90
|
+
else
|
91
|
+
raise CommandLine::FlagExpectsArgument.new(arg)
|
92
|
+
end
|
93
|
+
|
94
|
+
else
|
95
|
+
@flags[flag.name] = true
|
96
|
+
end
|
97
|
+
else
|
98
|
+
raise CommandLine::UnknownFlag.new(arg) unless @ignore_unknown_flags
|
99
|
+
end
|
100
|
+
else
|
101
|
+
if @begins_with_command && @command.nil?
|
102
|
+
@command = arg
|
103
|
+
else
|
104
|
+
@files << arg
|
105
|
+
end
|
106
|
+
end
|
107
|
+
i += 1
|
108
|
+
end
|
109
|
+
|
110
|
+
check_parsed_arguments!
|
111
|
+
|
112
|
+
return self
|
113
|
+
end
|
114
|
+
|
115
|
+
# Check if the parsed arguments meet their requirements.
|
116
|
+
# Raises CommandLineexception on error.
|
117
|
+
def check_parsed_arguments!
|
118
|
+
if @begins_with_command && @command.nil?
|
119
|
+
raise CommandLine::CommandMissing.new
|
120
|
+
end
|
121
|
+
|
122
|
+
if @required_files && @files.length < @required_files
|
123
|
+
raise CommandLine::FileMissing.new("You need at least #{@required_files} files")
|
124
|
+
end
|
125
|
+
|
126
|
+
end
|
127
|
+
end
|
128
|
+
|
129
|
+
end
|
@@ -0,0 +1,37 @@
|
|
1
|
+
module CommandLine
|
2
|
+
|
3
|
+
# Commandline parsing errors and exceptions
|
4
|
+
class Error < Exception
|
5
|
+
end
|
6
|
+
|
7
|
+
# Missing a required flag
|
8
|
+
class FlagMissing < CommandLine::Error
|
9
|
+
end
|
10
|
+
|
11
|
+
# Missing a required file
|
12
|
+
class FileMissing < CommandLine::Error
|
13
|
+
end
|
14
|
+
|
15
|
+
# Missing a required flag argument
|
16
|
+
class FlagExpectsArgument < CommandLine::Error
|
17
|
+
def initialize(flag)
|
18
|
+
super("#{flag} expects an argument!")
|
19
|
+
end
|
20
|
+
end
|
21
|
+
|
22
|
+
# Missing a required command
|
23
|
+
class CommandMissing < CommandLine::Error
|
24
|
+
def initialize(msg = "A command is missing")
|
25
|
+
super(msg)
|
26
|
+
end
|
27
|
+
|
28
|
+
end
|
29
|
+
|
30
|
+
# Encountered an unkown flag
|
31
|
+
class UnknownFlag < CommandLine::Error
|
32
|
+
def initialize(flag)
|
33
|
+
super("#{flag} not recognized as a valid flag!")
|
34
|
+
end
|
35
|
+
end
|
36
|
+
|
37
|
+
end
|
@@ -0,0 +1,51 @@
|
|
1
|
+
module CommandLine
|
2
|
+
|
3
|
+
# Argument flag handling.
|
4
|
+
class Flag
|
5
|
+
|
6
|
+
attr_reader :name
|
7
|
+
attr_reader :alias
|
8
|
+
attr_reader :argument
|
9
|
+
|
10
|
+
# Initialize new Flag
|
11
|
+
# <tt>name</tt> The name of the flag
|
12
|
+
# <tt>definition</tt> The definition of the flag.
|
13
|
+
def initialize(name, definition)
|
14
|
+
@name = name.to_s.gsub(/_/, '-').to_sym
|
15
|
+
@alias = definition[:alias].to_sym if definition[:alias]
|
16
|
+
@required = definition.has_key?(:required) && definition[:required] == true
|
17
|
+
@argument = definition[:expects] if definition[:expects]
|
18
|
+
end
|
19
|
+
|
20
|
+
# Argument representation of the flag (--fast)
|
21
|
+
def to_argument
|
22
|
+
"--#{@name}"
|
23
|
+
end
|
24
|
+
|
25
|
+
# Argument alias representation of the flag (-f)
|
26
|
+
def to_alias
|
27
|
+
"-#{@alias}"
|
28
|
+
end
|
29
|
+
|
30
|
+
# Check if flag has an alias
|
31
|
+
def has_alias?
|
32
|
+
!@alias.nil?
|
33
|
+
end
|
34
|
+
|
35
|
+
# Check if flag is optional
|
36
|
+
def optional?
|
37
|
+
!@required
|
38
|
+
end
|
39
|
+
|
40
|
+
# Check if flag is required
|
41
|
+
def required?
|
42
|
+
@required
|
43
|
+
end
|
44
|
+
|
45
|
+
# Check if flag expects an argument (Are you talking to me?)
|
46
|
+
def expects_argument?
|
47
|
+
!@argument.nil?
|
48
|
+
end
|
49
|
+
end
|
50
|
+
|
51
|
+
end
|