greenhat 0.1.4 → 0.3.1
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.
- checksums.yaml +4 -4
- data/README.md +2 -7
- data/lib/greenhat/accessors/disk.rb +58 -2
- data/lib/greenhat/accessors/gitlab.rb +75 -0
- data/lib/greenhat/accessors/memory.rb +10 -10
- data/lib/greenhat/accessors/process.rb +10 -1
- data/lib/greenhat/cli.rb +128 -57
- data/lib/greenhat/color.rb +27 -0
- data/lib/greenhat/logbot.rb +9 -9
- data/lib/greenhat/settings.rb +51 -3
- data/lib/greenhat/shell/args.rb +146 -0
- data/lib/greenhat/shell/cat.rb +25 -73
- data/lib/greenhat/shell/color_string.rb +43 -0
- data/lib/greenhat/shell/disk.rb +30 -42
- data/lib/greenhat/shell/faststats.rb +80 -61
- data/lib/greenhat/shell/filter_help.rb +143 -0
- data/lib/greenhat/shell/gitlab.rb +61 -2
- data/lib/greenhat/shell/help.rb +98 -15
- data/lib/greenhat/shell/list.rb +46 -0
- data/lib/greenhat/shell/log.rb +78 -203
- data/lib/greenhat/shell/page.rb +39 -0
- data/lib/greenhat/shell/process.rb +57 -2
- data/lib/greenhat/shell/report.rb +70 -60
- data/lib/greenhat/shell/shell_helper.rb +601 -0
- data/lib/greenhat/shell.rb +27 -13
- data/lib/greenhat/thing/file_types.rb +76 -8
- data/lib/greenhat/thing/formatters/json_shellwords.rb +0 -3
- data/lib/greenhat/thing/formatters/nginx.rb +44 -0
- data/lib/greenhat/thing/formatters/syslog.rb +39 -0
- data/lib/greenhat/thing/helpers.rb +4 -4
- data/lib/greenhat/thing/kind.rb +9 -2
- data/lib/greenhat/thing/spinner.rb +3 -3
- data/lib/greenhat/thing.rb +3 -3
- data/lib/greenhat/tty/columns.rb +44 -0
- data/lib/greenhat/version.rb +1 -1
- data/lib/greenhat.rb +15 -14
- metadata +30 -20
- data/lib/greenhat/shell/helper.rb +0 -514
data/lib/greenhat/shell/disk.rb
CHANGED
@@ -8,60 +8,48 @@ module GreenHat
|
|
8
8
|
Cli.help
|
9
9
|
end
|
10
10
|
|
11
|
-
|
12
|
-
|
13
|
-
|
11
|
+
def self.help
|
12
|
+
puts "\u2500".pastel(:cyan) * 22
|
13
|
+
puts "#{'Disk'.pastel(:yellow)} - Storage Helper"
|
14
|
+
puts "\u2500".pastel(:cyan) * 22
|
15
|
+
|
16
|
+
ShellHelper.common_opts
|
17
|
+
|
18
|
+
puts 'Command Summary'.pastel(:blue)
|
19
|
+
puts ' df'.pastel(:green)
|
20
|
+
puts " Raw #{'df'.pastel(:cyan)} output"
|
21
|
+
puts
|
22
|
+
puts ' free'.pastel(:green)
|
23
|
+
puts ' Formatted / Bar Output'
|
24
|
+
puts
|
14
25
|
end
|
15
26
|
|
16
|
-
#
|
17
|
-
def self.
|
18
|
-
|
19
|
-
|
20
|
-
puts file.friendly_name
|
21
|
-
disks = file.data.sort_by { |x| x.use.to_i }.reverse.reject { |x| x.filesystem.include? 'tmpfs' }
|
27
|
+
# Easy Show All
|
28
|
+
def self.df(raw = {})
|
29
|
+
# Extract Args
|
30
|
+
files_list, flags, _args = Args.parse(raw)
|
22
31
|
|
23
|
-
|
24
|
-
|
25
|
-
# pad_used = GreenHat::Disk.max_padding(disks, :used)
|
26
|
-
# pad_avail = GreenHat::Disk.max_padding(disks, :avail)
|
32
|
+
# Collect Files
|
33
|
+
files = ShellHelper.files(files_list, GreenHat::Disk.df, flags)
|
27
34
|
|
28
|
-
|
35
|
+
# Output
|
36
|
+
ShellHelper.file_output(files, flags)
|
37
|
+
end
|
29
38
|
|
30
|
-
|
31
|
-
|
32
|
-
|
33
|
-
'Size'.ljust(pad_size).colorize(:magenta),
|
34
|
-
'Used'.ljust(pad_used).colorize(:cyan),
|
35
|
-
'Avail'.ljust(pad_avail).colorize(:white),
|
36
|
-
'% Use'.ljust(pad_avail).colorize(:green)
|
37
|
-
].join
|
39
|
+
def self.free(raw = {})
|
40
|
+
# Extract Args
|
41
|
+
files_list, flags, _args = Args.parse(raw)
|
38
42
|
|
39
|
-
|
40
|
-
|
41
|
-
# Pretty Disk Use
|
42
|
-
use = [
|
43
|
-
disk.use.rjust(5).ljust(5).colorize(:green),
|
44
|
-
' ['.colorize(:blue),
|
45
|
-
('=' * (disk.use.to_i / 2)).colorize(:green),
|
46
|
-
' ' * (50 - disk.use.to_i / 2),
|
47
|
-
']'.colorize(:blue)
|
48
|
-
].join
|
43
|
+
# Collect Files
|
44
|
+
files = ShellHelper.files(files_list, GreenHat::Disk.df, flags)
|
49
45
|
|
50
|
-
|
51
|
-
|
52
|
-
disk.mounted_on.ljust(pad_mount).colorize(:blue),
|
53
|
-
disk[:size].to_s.ljust(pad_size).colorize(:magenta),
|
54
|
-
disk.used.to_s.ljust(pad_used).colorize(:cyan),
|
55
|
-
disk.avail.to_s.ljust(pad_avail).colorize(:white),
|
56
|
-
use
|
57
|
-
].join
|
58
|
-
end
|
46
|
+
files.each do |file|
|
47
|
+
puts GreenHat::Disk.format_output(file, true)
|
59
48
|
|
60
49
|
# File End Loop / Break
|
61
50
|
puts
|
62
51
|
end
|
63
52
|
end
|
64
|
-
# rubocop:enable Metrics/MethodLength,Metrics/BlockLength
|
65
53
|
# ------------------------------------------------------------------------
|
66
54
|
end
|
67
55
|
end
|
@@ -5,47 +5,65 @@ module GreenHat
|
|
5
5
|
module Faststats
|
6
6
|
# rubocop:disable Metrics/MethodLength)
|
7
7
|
def self.help
|
8
|
-
puts "\u2500".
|
9
|
-
puts "Gimme #{'Performance Stats'.
|
10
|
-
puts "\u2500".
|
8
|
+
puts "\u2500".pastel(:cyan) * 25
|
9
|
+
puts "Gimme #{'Performance Stats'.pastel(:yellow)}"
|
10
|
+
puts "\u2500".pastel(:cyan) * 25
|
11
11
|
|
12
|
-
puts 'General'.
|
13
|
-
puts " Any double dash arguments (e.g. #{'--color-output'.
|
14
|
-
puts " See #{'`fast-stats --help`'.
|
12
|
+
puts 'General'.pastel(:blue)
|
13
|
+
puts " Any double dash arguments (e.g. #{'--color-output'.pastel(:green)}) are passed directly to fast-stats"
|
14
|
+
puts " See #{'`fast-stats --help`'.pastel(:bright_black)} for all available options"
|
15
15
|
puts
|
16
16
|
|
17
|
-
puts 'Common Options'.
|
18
|
-
puts ' --raw'.
|
17
|
+
puts 'Common Options'.pastel(:blue)
|
18
|
+
puts ' --raw'.pastel(:green)
|
19
19
|
puts ' Do not use less/paging'
|
20
|
-
puts ' --
|
21
|
-
puts '
|
22
|
-
puts ' --sort'.
|
20
|
+
puts ' --search'.pastel(:green)
|
21
|
+
puts ' Case-insensitive search of controller/method/worker field'
|
22
|
+
puts ' --sort'.pastel(:green)
|
23
23
|
puts ' count,fail,max,median,min,p95,p99,rps,score'
|
24
|
-
puts ' --limit'.
|
24
|
+
puts ' --limit'.pastel(:green)
|
25
25
|
puts ' The number of rows to print'
|
26
|
-
puts ' --verbose'.
|
26
|
+
puts ' --verbose'.pastel(:green)
|
27
27
|
puts ' Prints the component details of the maximum, P99, P95, and median events by total duration for each'
|
28
28
|
puts
|
29
29
|
|
30
|
-
puts 'Commands'.
|
31
|
-
puts 'ls'.
|
30
|
+
puts 'Commands'.pastel(:blue)
|
31
|
+
puts 'ls'.pastel(:green)
|
32
32
|
puts ' List available files'
|
33
33
|
puts ' Options'
|
34
34
|
puts ' -a, --all, show all files including source'
|
35
35
|
puts
|
36
36
|
|
37
|
-
puts '<file names+>'.
|
37
|
+
puts '<file names+>'.pastel(:green)
|
38
38
|
puts ' Print any file names'
|
39
39
|
puts ' Ex: `gitaly/current`'
|
40
40
|
puts ' Ex: `gitlab-rails/api_json.log gitlab-rails/production_json.log --raw`'
|
41
41
|
puts
|
42
42
|
|
43
|
-
puts 'top'.
|
43
|
+
puts 'top'.pastel(:green)
|
44
44
|
puts ' Print a summary of top items in a category'
|
45
45
|
puts
|
46
46
|
|
47
|
-
puts 'errors'.
|
47
|
+
puts 'errors'.pastel(:green)
|
48
48
|
puts ' Show summary of errors captured in log'
|
49
|
+
puts
|
50
|
+
|
51
|
+
puts 'Examples'.pastel(:blue)
|
52
|
+
puts ' Default'.pastel(:bright_white)
|
53
|
+
puts ' gitaly/current'
|
54
|
+
puts ' --raw gitlab-rails/production_json.log'
|
55
|
+
puts ' gitlab-rails/api_json.log --sort=count'
|
56
|
+
puts ' --search=pipeline sidekiq/current'
|
57
|
+
puts
|
58
|
+
|
59
|
+
puts ' Top'.pastel(:bright_white)
|
60
|
+
puts ' top gitaly/current'
|
61
|
+
puts ' top --limit=5 sidekiq/current'
|
62
|
+
puts
|
63
|
+
|
64
|
+
puts ' Errors'.pastel(:bright_white)
|
65
|
+
puts ' errors gitaly/current'
|
66
|
+
puts ' errors gitaly/current --no-border'
|
49
67
|
end
|
50
68
|
# rubocop:enable Metrics/MethodLength)
|
51
69
|
|
@@ -67,7 +85,7 @@ module GreenHat
|
|
67
85
|
if all
|
68
86
|
puts "- #{log.friendly_name}"
|
69
87
|
else
|
70
|
-
puts "- #{log.name.
|
88
|
+
puts "- #{log.name.pastel(:yellow)}"
|
71
89
|
end
|
72
90
|
end
|
73
91
|
end
|
@@ -77,18 +95,10 @@ module GreenHat
|
|
77
95
|
end
|
78
96
|
|
79
97
|
# Vanilla Fast Stats
|
80
|
-
def self.default(
|
81
|
-
|
82
|
-
file_list, opts, args = ShellHelper.param_parse(file_list)
|
83
|
-
|
84
|
-
cmd = opts.map { |opt| "--#{opt.field}=#{opt.value}" }.join(' ')
|
85
|
-
cmd += args.keys.map { |arg| "--#{arg}" }.join(' ')
|
98
|
+
def self.default(raw, _other = nil)
|
99
|
+
files, flags, cmd = ShellHelper::Faststats.parse(raw)
|
86
100
|
|
87
|
-
|
88
|
-
file_list = ShellHelper.prepare_list(file_list, ShellHelper::Faststats.things)
|
89
|
-
|
90
|
-
# Convert to Things
|
91
|
-
files = ShellHelper.find_things(file_list)
|
101
|
+
LogBot.debug('FastStats CMD', cmd) if ENV['DEBUG']
|
92
102
|
|
93
103
|
results = ShellHelper.file_process(files) do |file|
|
94
104
|
[
|
@@ -98,7 +108,7 @@ module GreenHat
|
|
98
108
|
]
|
99
109
|
end
|
100
110
|
|
101
|
-
ShellHelper.show(results.flatten,
|
111
|
+
ShellHelper.show(results.flatten, flags)
|
102
112
|
end
|
103
113
|
|
104
114
|
# ========================================================================
|
@@ -110,21 +120,10 @@ module GreenHat
|
|
110
120
|
# --limit=X
|
111
121
|
# --sort=count,fail,max,median,min,p95,p99,rps,score
|
112
122
|
# ========================================================================
|
113
|
-
def self.top(
|
114
|
-
|
115
|
-
log_list, opts, args = ShellHelper.param_parse(log_list)
|
116
|
-
|
117
|
-
# Default to color output
|
118
|
-
args['color-output'] ||= true
|
119
|
-
|
120
|
-
cmd = opts.map { |opt| "--#{opt.field}=#{opt.value}" }.join(' ')
|
121
|
-
cmd += args.keys.map { |arg| "--#{arg}" }.join(' ')
|
122
|
-
|
123
|
-
# Prepare Log List
|
124
|
-
log_list = ShellHelper.prepare_list(log_list, ShellHelper::Faststats.things)
|
123
|
+
def self.top(raw)
|
124
|
+
files, flags, cmd = ShellHelper::Faststats.parse(raw)
|
125
125
|
|
126
|
-
|
127
|
-
files = ShellHelper.find_things(log_list)
|
126
|
+
LogBot.debug('FastStats CMD', cmd) if ENV['DEBUG']
|
128
127
|
|
129
128
|
results = ShellHelper.file_process(files) do |file|
|
130
129
|
[
|
@@ -134,28 +133,19 @@ module GreenHat
|
|
134
133
|
]
|
135
134
|
end
|
136
135
|
|
137
|
-
ShellHelper.show(results.flatten,
|
136
|
+
ShellHelper.show(results.flatten, flags)
|
138
137
|
end
|
139
138
|
# ========================================================================
|
140
139
|
|
141
140
|
# ========================================================================
|
142
141
|
# ===== [ Fast Stats - Errors ] ====================
|
143
142
|
# ========================================================================
|
144
|
-
def self.errors(
|
145
|
-
#
|
146
|
-
|
143
|
+
def self.errors(raw)
|
144
|
+
# Add Color Output
|
145
|
+
raw.push '--color-output'
|
146
|
+
files, flags, cmd = ShellHelper::Faststats.parse(raw)
|
147
147
|
|
148
|
-
|
149
|
-
args['color-output'] ||= true
|
150
|
-
|
151
|
-
cmd = opts.map { |opt| "--#{opt.field}=#{opt.value}" }.join(' ')
|
152
|
-
cmd += args.keys.map { |arg| "--#{arg}" }.join(' ')
|
153
|
-
|
154
|
-
# Prepare Log List
|
155
|
-
log_list = ShellHelper.prepare_list(log_list, ShellHelper::Faststats.things)
|
156
|
-
|
157
|
-
# Convert to Things
|
158
|
-
files = ShellHelper.find_things(log_list)
|
148
|
+
LogBot.debug('FastStats CMD', cmd) if ENV['DEBUG']
|
159
149
|
|
160
150
|
results = ShellHelper.file_process(files) do |file|
|
161
151
|
[
|
@@ -165,7 +155,7 @@ module GreenHat
|
|
165
155
|
]
|
166
156
|
end
|
167
157
|
|
168
|
-
ShellHelper.show(results.flatten,
|
158
|
+
ShellHelper.show(results.flatten, flags)
|
169
159
|
end
|
170
160
|
# ========================================================================
|
171
161
|
end
|
@@ -176,6 +166,35 @@ module GreenHat
|
|
176
166
|
module ShellHelper
|
177
167
|
# Hide from Commands
|
178
168
|
module Faststats
|
169
|
+
# Default Settings from arg parse that won't work
|
170
|
+
def self.invalid_settings
|
171
|
+
%i[page round truncate logic fuzzy_file_match]
|
172
|
+
end
|
173
|
+
|
174
|
+
def self.parse(raw)
|
175
|
+
file_list, flags, args = Args.parse(raw, invalid_settings)
|
176
|
+
cmd = args.map { |opt| "--#{opt.field}=#{opt.value}" }.join(' ')
|
177
|
+
cmd += flags.map do |flag, value|
|
178
|
+
# Don't Include Raw
|
179
|
+
next if flag == :raw
|
180
|
+
|
181
|
+
case value
|
182
|
+
when true then "--#{flag}"
|
183
|
+
when Array then "--#{flag}=#{value.join}"
|
184
|
+
else
|
185
|
+
"--#{flag}=#{value}"
|
186
|
+
end
|
187
|
+
end.join(' ')
|
188
|
+
|
189
|
+
# Prepare Log List
|
190
|
+
file_list = ShellHelper.prepare_list(file_list, ShellHelper::Faststats.things, flags)
|
191
|
+
|
192
|
+
# Convert to Things
|
193
|
+
files = ShellHelper.find_things(file_list)
|
194
|
+
|
195
|
+
[files, flags, cmd]
|
196
|
+
end
|
197
|
+
|
179
198
|
def self.files
|
180
199
|
%w[
|
181
200
|
production_json
|
@@ -0,0 +1,143 @@
|
|
1
|
+
module GreenHat
|
2
|
+
# CLI Helper
|
3
|
+
module ShellHelper
|
4
|
+
# Unify Filter / Filter Help
|
5
|
+
module Filter
|
6
|
+
# rubocop:disable Metrics/MethodLength
|
7
|
+
def self.help
|
8
|
+
puts "\u2500".pastel(:cyan) * 20
|
9
|
+
puts 'Filter'.pastel(:yellow)
|
10
|
+
puts "\u2500".pastel(:cyan) * 20
|
11
|
+
|
12
|
+
puts 'Options'.pastel(:blue)
|
13
|
+
puts '--raw'.pastel(:green)
|
14
|
+
puts ' Do not use less/paging'
|
15
|
+
puts
|
16
|
+
|
17
|
+
puts '--page'.pastel(:green)
|
18
|
+
puts ' Specifically enable or disable paging'
|
19
|
+
puts ' E.g. --page (default to true), --page=true, --page=false'
|
20
|
+
puts
|
21
|
+
|
22
|
+
puts '--round'.pastel(:green)
|
23
|
+
puts ' Attempt to round all integers. Default: 2.'
|
24
|
+
puts ' E.g. --round, --round=3, --round=0'
|
25
|
+
puts
|
26
|
+
|
27
|
+
puts '--limit'.pastel(:green)
|
28
|
+
puts ' Limit total output lines. Disabled by default. Default without value is based on screen height'
|
29
|
+
puts ' E.g. --limit, --limit=5'
|
30
|
+
puts
|
31
|
+
|
32
|
+
puts '--json'.pastel(:green)
|
33
|
+
puts ' Print output back into JSON'
|
34
|
+
puts
|
35
|
+
|
36
|
+
puts '--or'.pastel(:green)
|
37
|
+
puts ' Filters will use OR instead of AND (all match vs any match)'
|
38
|
+
puts
|
39
|
+
|
40
|
+
puts '--total'.pastel(:green)
|
41
|
+
puts ' Print only total count of matching entries'
|
42
|
+
puts
|
43
|
+
|
44
|
+
puts '--slice'.pastel(:green)
|
45
|
+
puts ' Extract specific fields from entries (slice multiple with comma)'
|
46
|
+
puts ' Ex: --slice=path or --slice=path,params'
|
47
|
+
puts
|
48
|
+
|
49
|
+
puts '--except'.pastel(:green)
|
50
|
+
puts ' Exclude specific fields (except multiple with comma)'
|
51
|
+
puts ' Ex: --except=params --except=params,path'
|
52
|
+
puts
|
53
|
+
|
54
|
+
puts '--exists'.pastel(:green)
|
55
|
+
puts ' Ensure field exists regardless of contents'
|
56
|
+
puts ' Ex: --exists=params --exists=params,path'
|
57
|
+
puts
|
58
|
+
|
59
|
+
puts '--stats'.pastel(:green)
|
60
|
+
puts ' Order/Count occurrances by field'
|
61
|
+
puts ' Ex: --stats=params --except=params,path'
|
62
|
+
puts
|
63
|
+
|
64
|
+
puts '--uniq'.pastel(:green)
|
65
|
+
puts ' Show unique values only'
|
66
|
+
puts ' Ex: --uniq=params --uniq=params,path'
|
67
|
+
puts
|
68
|
+
|
69
|
+
puts '--pluck'.pastel(:green)
|
70
|
+
puts ' Extract values from entries'
|
71
|
+
puts ' Ex: --pluck=params --pluck=params,path'
|
72
|
+
puts
|
73
|
+
|
74
|
+
puts '--archive'.pastel(:green)
|
75
|
+
puts ' Limit to specific archvie name (partial matching /inclusive). Matching SOS tar.gz name'
|
76
|
+
puts ' Ex: --archive=dev-gitlab_20210622154626, --archive=202106,202107'
|
77
|
+
puts
|
78
|
+
|
79
|
+
puts '--sort'.pastel(:green)
|
80
|
+
puts ' Sort by multiple fields'
|
81
|
+
puts ' Ex: --sort=duration_s,db_duration_s'
|
82
|
+
puts
|
83
|
+
|
84
|
+
puts '--reverse'.pastel(:green)
|
85
|
+
puts ' Reverse all results'
|
86
|
+
puts ' Ex: --reverse'
|
87
|
+
puts
|
88
|
+
|
89
|
+
puts '--combine'.pastel(:green)
|
90
|
+
puts ' Omit archive identifier dividers. Useful with sort or time filters'
|
91
|
+
puts ' Ex: --combine'
|
92
|
+
puts
|
93
|
+
|
94
|
+
puts '--case'.pastel(:green)
|
95
|
+
puts ' Exact case match. Defaults to case insensitive'
|
96
|
+
puts ' Ex: --case; --name=Jon, --name=jane --case'
|
97
|
+
puts
|
98
|
+
|
99
|
+
puts '--exact'.pastel(:green)
|
100
|
+
puts ' Exact parameter/value match. Defaults to partial match'
|
101
|
+
puts ' Ex: --field=CommonPartial --exact'
|
102
|
+
puts
|
103
|
+
|
104
|
+
puts '--start'.pastel(:green)
|
105
|
+
puts ' Show events after specified time. Filtered by the `time` field'
|
106
|
+
puts ' Use with `--end` for between selections'
|
107
|
+
puts ' Ex: log filter --start="2021-06-22 14:44 UTC" --end="2021-06-22 14:45 UTC"'
|
108
|
+
puts
|
109
|
+
|
110
|
+
puts '--end'.pastel(:green)
|
111
|
+
puts ' Show events before specified time. Filtered by the `time` field'
|
112
|
+
puts ' Use with `--start` for between selections'
|
113
|
+
puts ' Ex: log filter --end="2021-06-22"'
|
114
|
+
puts
|
115
|
+
|
116
|
+
puts '--truncate'.pastel(:green)
|
117
|
+
puts ' Truncate field length. On by default (4 rows). Performance issues!'
|
118
|
+
puts ' Disable with --truncate=0'.pastel(:bright_red)
|
119
|
+
puts ' Ex: --truncate=200, --truncate=2048"'
|
120
|
+
puts
|
121
|
+
|
122
|
+
puts 'Field Searching'.pastel(:blue)
|
123
|
+
puts ' --[key]=[value]'
|
124
|
+
puts ' Search in key for value'
|
125
|
+
puts ' Example: --path=mirror/pull'
|
126
|
+
puts
|
127
|
+
|
128
|
+
puts 'Search specific logs'.pastel(:blue)
|
129
|
+
puts ' Any non dash parameters will be the log list to search from'
|
130
|
+
puts " Ex: log filter --path=api sidekiq/current (hint: use `#{'ls'.pastel(:yellow)}` for log names"
|
131
|
+
puts
|
132
|
+
|
133
|
+
puts 'Example Queries'.pastel(:blue)
|
134
|
+
puts " Also see #{'examples'.pastel(:bright_blue)} for even more examples"
|
135
|
+
puts ' log filter --class=BuildFinishedWorker sidekiq/current --slice=time,message'
|
136
|
+
puts ' log filter gitlab-rails/api_json.log --slice=ua --uniq=ua --ua=gitlab-runner'
|
137
|
+
|
138
|
+
puts
|
139
|
+
end
|
140
|
+
# rubocop:enable Metrics/MethodLength
|
141
|
+
end
|
142
|
+
end
|
143
|
+
end
|