logstash-cli 0.0.2 → 0.0.3
Sign up to get free protection for your applications and to get access to all the features.
- data/README.md +42 -1
- data/lib/logstash-cli/cli.rb +29 -4
- data/lib/logstash-cli/command/grep.rb +2 -1
- data/lib/logstash-cli/command/tail.rb +67 -0
- data/lib/logstash-cli/command.rb +7 -0
- data/lib/logstash-cli/version.rb +1 -1
- data/logstashcli.gemspec +2 -2
- metadata +26 -11
- data/lib/logstash-cli/command/live.rb +0 -139
data/README.md
CHANGED
@@ -33,6 +33,8 @@ Mucho inspired by a gist of the eminent @lusis - <https://gist.github.com/138807
|
|
33
33
|
|
34
34
|
## Commandline Options
|
35
35
|
|
36
|
+
### Grep
|
37
|
+
|
36
38
|
Usage:
|
37
39
|
logstash-cli grep PATTERN
|
38
40
|
|
@@ -59,15 +61,54 @@ Mucho inspired by a gist of the eminent @lusis - <https://gist.github.com/138807
|
|
59
61
|
|
60
62
|
Search logstash for a pattern
|
61
63
|
|
64
|
+
### Tail
|
65
|
+
|
66
|
+
Usage:
|
67
|
+
logstash-cli tail
|
68
|
+
|
69
|
+
Options:
|
70
|
+
[--host=HOST] # Host to connect to AMQP
|
71
|
+
# Default: localhost
|
72
|
+
--amqpurl, [--url=URL] # Alternate way to specify settings via an AMQP Url f.i. amqp://logstash:foopass@localhost:5672.
|
73
|
+
This takes precendence over other settings. Note that username and password need to be percentage encoded(URL encoded) in case of special characters
|
74
|
+
[--auto-delete] # Autodelete Exchange or not
|
75
|
+
[--vhost=VHOST] # VHost to connect to AMQP
|
76
|
+
# Default: /
|
77
|
+
[--persistent] # Persistent Exchange or not
|
78
|
+
[--ssl] # Enable SSL to connect to AMQP
|
79
|
+
[--user=USER] # User to connect to AMQP
|
80
|
+
# Default: logstash
|
81
|
+
[--meta=META] # Meta Logstash fields to show
|
82
|
+
# Default: timestamp,type,message
|
83
|
+
[--format=FORMAT] # Format to use for exporting (plain,csv,json)
|
84
|
+
# Default: csv
|
85
|
+
[--key=KEY] # Routing key
|
86
|
+
# Default: #
|
87
|
+
[--port=PORT] # Port to connect to AMQP
|
88
|
+
# Default: 5672
|
89
|
+
[--exchange=EXCHANGE] # Exchange name
|
90
|
+
# Default: rawlogs
|
91
|
+
[--password=PASSWORD] # Password to connect to AMQP
|
92
|
+
# Default: foo
|
93
|
+
[--delim=DELIM] # csv delimiter
|
94
|
+
# Default: |
|
95
|
+
[--exchange-type=EXCHANGE_TYPE] # Exchange Type
|
96
|
+
# Default: direct
|
97
|
+
[--durable] # Durable Exchange or not
|
98
|
+
|
99
|
+
Stream a live feed via AMQP
|
100
|
+
|
101
|
+
|
62
102
|
## Examples
|
63
103
|
|
64
104
|
$ logstash-cli grep --esurl="http://logger-1.jedi.be:9200" '@message:jedi4ever AND program:sshd' --last 5d --format csv --delim ':'
|
65
105
|
|
106
|
+
$ logstash-cli tail --amqpurl="amqp://logger-1.jedi.be:5672" --key="program.sshd"
|
107
|
+
|
66
108
|
## TODO
|
67
109
|
|
68
110
|
- find a way to query existing instances
|
69
111
|
- specify last 15m
|
70
112
|
- find a way to get the results by streaming instead of loading all in memory (maybe pagination will help here)
|
71
|
-
- live tailing the output
|
72
113
|
- produce ascii histograms
|
73
114
|
- or sparklines
|
data/lib/logstash-cli/cli.rb
CHANGED
@@ -1,10 +1,11 @@
|
|
1
1
|
require 'rubygems'
|
2
2
|
require 'bundler/setup'
|
3
3
|
require 'rack'
|
4
|
+
require 'amqp'
|
4
5
|
require 'tire'
|
5
6
|
require 'time'
|
6
7
|
require 'fastercsv'
|
7
|
-
require 'logstash-cli/command
|
8
|
+
require 'logstash-cli/command'
|
8
9
|
|
9
10
|
module LogstashCli
|
10
11
|
class CLI < Thor
|
@@ -16,15 +17,39 @@ module LogstashCli
|
|
16
17
|
method_option :index_prefix , :default => "logstash-", :desc => "Logstash index prefix"
|
17
18
|
method_option :from , :default => "#{Time.now.strftime('%Y-%m-%d')}", :desc => "Begin date"
|
18
19
|
method_option :to, :default => "#{Time.now.strftime('%Y-%m-%d')}", :desc => "End date"
|
19
|
-
method_option :format , :default => 'csv', :desc => "Format to use for exporting"
|
20
|
+
method_option :format , :default => 'csv', :desc => "Format to use for exporting (plain,csv,json)"
|
20
21
|
method_option :size , :default => 500, :desc => "Number of results to return"
|
21
22
|
method_option :last , :default => nil, :desc => "Specify period since now f.i. 1d"
|
22
23
|
method_option :meta , :default => "type,message", :desc => "Meta Logstash fields to show"
|
23
|
-
method_option :fields , :default => "
|
24
|
-
method_option :delim , :default => "|", :desc => "csv delimiter"
|
24
|
+
method_option :fields , :default => "", :desc => "Logstash Fields to show"
|
25
|
+
method_option :delim , :default => "|", :desc => "plain or csv delimiter"
|
26
|
+
|
25
27
|
def grep(pattern)
|
26
28
|
_grep(pattern,options)
|
27
29
|
end
|
28
30
|
|
31
|
+
desc "tail", "Stream a live feed via AMQP"
|
32
|
+
method_option :url, :desc => "Alternate way to specify settings via an AMQP Url f.i. amqp://logstash:foopass@localhost:5672. \n This takes precendence over other settings. Note that username and password need to be percentage encoded(URL encoded) in case of special characters",:aliases => "\--amqpurl"
|
33
|
+
method_option :user, :default => 'logstash', :desc => "User to connect to AMQP"
|
34
|
+
method_option :password, :default => 'foo', :desc => "Password to connect to AMQP"
|
35
|
+
method_option :vhost, :default => '/', :desc => "VHost to connect to AMQP"
|
36
|
+
method_option :port, :default => 5672, :desc => "Port to connect to AMQP"
|
37
|
+
method_option :host, :default => 'localhost' , :desc => "Host to connect to AMQP"
|
38
|
+
method_option :ssl, :default => false , :desc => "Enable SSL to connect to AMQP", :type => :boolean
|
39
|
+
|
40
|
+
method_option :exchange, :default => 'rawlogs', :desc => "Exchange name"
|
41
|
+
method_option :exchange_type, :default => 'direct', :desc => "Exchange Type"
|
42
|
+
method_option :durable, :default => false, :desc => "Durable Exchange or not", :type => :boolean
|
43
|
+
method_option :auto_delete, :default => false, :desc => "Autodelete Exchange or not" , :type => :boolean
|
44
|
+
method_option :persistent, :default => false, :desc => "Persistent Exchange or not", :type => :boolean
|
45
|
+
method_option :key, :default => '#', :desc => "Routing key"
|
46
|
+
method_option :format , :default => 'csv', :desc => "Format to use for exporting (plain,csv,json)"
|
47
|
+
method_option :meta, :default => "timestamp,type,message", :desc => "Meta Logstash fields to show"
|
48
|
+
method_option :delim, :default => "|", :desc => "csv delimiter"
|
49
|
+
|
50
|
+
def tail()
|
51
|
+
_tail(options)
|
52
|
+
end
|
53
|
+
|
29
54
|
end
|
30
55
|
end
|
@@ -2,7 +2,7 @@ require 'date'
|
|
2
2
|
|
3
3
|
require 'yajl/json_gem'
|
4
4
|
|
5
|
-
module
|
5
|
+
module Grep
|
6
6
|
|
7
7
|
def _grep(pattern,options)
|
8
8
|
es_url = options[:esurl]
|
@@ -80,6 +80,7 @@ module LogstashCli::Command
|
|
80
80
|
output = case options[:format]
|
81
81
|
when 'csv' then result.to_csv({:col_sep => options[:delim]})
|
82
82
|
when 'json' then result.to_json
|
83
|
+
when 'plain' then result.join(options[:delim])
|
83
84
|
end
|
84
85
|
#tstamp = Time.iso8601(res[:@timestamp]).localtime.iso8601
|
85
86
|
|
@@ -0,0 +1,67 @@
|
|
1
|
+
require 'yajl/json_gem'
|
2
|
+
|
3
|
+
module Tail
|
4
|
+
|
5
|
+
def _tail(options)
|
6
|
+
amqp_url = options[:url]
|
7
|
+
|
8
|
+
amqp_user = options[:user]
|
9
|
+
amqp_password = options[:password]
|
10
|
+
amqp_vhost = options[:vhost]
|
11
|
+
amqp_port = options[:port]
|
12
|
+
amqp_host = options[:host]
|
13
|
+
amqp_ssl = options[:ssl]
|
14
|
+
|
15
|
+
exchange_name = options[:exchange]
|
16
|
+
exchange_type = options[:exchange_type]
|
17
|
+
persistent = options[:persistent]
|
18
|
+
durable = options[:durable]
|
19
|
+
auto_delete = options[:autodelete]
|
20
|
+
routing_key = options[:key]
|
21
|
+
metafields = options[:meta].split(',')
|
22
|
+
|
23
|
+
begin
|
24
|
+
#connection = AMQP.connect(AMQP_OPTS.merge(:username => "amqp_gem", :password => "amqp_gem_password", :vhost => "amqp_gem_testbed"))
|
25
|
+
settings= { :host => amqp_host, :vhost => amqp_vhost, :port => amqp_port,
|
26
|
+
:user => amqp_user, :password => amqp_password ,
|
27
|
+
:ssl => amqp_ssl }
|
28
|
+
|
29
|
+
# Aqmp url can override settings
|
30
|
+
unless amqp_url.nil?
|
31
|
+
settings = aqmp_url
|
32
|
+
end
|
33
|
+
|
34
|
+
AMQP.start(settings) do |connection, open_ok|
|
35
|
+
channel = AMQP::Channel.new(connection, :auto_recovery => true)
|
36
|
+
|
37
|
+
channel.queue("", :auto_delete => auto_delete, :peristent => persistent , :durable => durable) do |queue, declare_ok|
|
38
|
+
queue.bind(exchange_name, :routing_key => routing_key)
|
39
|
+
queue.subscribe do |payload|
|
40
|
+
parsed_message = JSON.parse(payload)
|
41
|
+
result = Array.new
|
42
|
+
|
43
|
+
metafields.each do |metafield|
|
44
|
+
result << parsed_message["@#{metafield}"]
|
45
|
+
end
|
46
|
+
|
47
|
+
output = case options[:format]
|
48
|
+
when 'csv' then result.to_csv({:col_sep => options[:delim]})
|
49
|
+
when 'json' then result.to_json
|
50
|
+
when 'plain' then result.join(options[:delim])
|
51
|
+
end
|
52
|
+
|
53
|
+
puts output
|
54
|
+
result = []
|
55
|
+
end
|
56
|
+
end
|
57
|
+
end
|
58
|
+
rescue AMQP::PossibleAuthenticationFailureError => ex
|
59
|
+
puts "Possible Authentication error:\nthe AMQP connection URL used is #{amqp_url}\n\nDetail Info:\n#{ex}"
|
60
|
+
exit -1
|
61
|
+
rescue Exception => ex
|
62
|
+
puts "Error occured: #{ex}"
|
63
|
+
exit -1
|
64
|
+
end
|
65
|
+
trap("INT") { puts "Shutting down..."; connection.close { EM.stop };exit }
|
66
|
+
end
|
67
|
+
end
|
data/lib/logstash-cli/version.rb
CHANGED
data/logstashcli.gemspec
CHANGED
@@ -9,14 +9,14 @@ Gem::Specification.new do |s|
|
|
9
9
|
s.email = ["patrick.debois@jedi.be"]
|
10
10
|
s.homepage = "http://github.com/jedi4ever/logstash-cli/"
|
11
11
|
s.summary = %q{CLI interface to logstash}
|
12
|
-
s.description = %q{CLI
|
12
|
+
s.description = %q{CLI interface to logstash}
|
13
13
|
|
14
14
|
s.required_rubygems_version = ">= 1.3.6"
|
15
15
|
s.rubyforge_project = "logstash-cli"
|
16
16
|
|
17
17
|
s.add_dependency "tire"
|
18
18
|
s.add_dependency "thor"
|
19
|
-
|
19
|
+
s.add_dependency "amqp"
|
20
20
|
s.add_dependency "rack"
|
21
21
|
s.add_dependency "yajl-ruby"
|
22
22
|
s.add_dependency "fastercsv"
|
metadata
CHANGED
@@ -1,13 +1,13 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: logstash-cli
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
hash:
|
4
|
+
hash: 25
|
5
5
|
prerelease:
|
6
6
|
segments:
|
7
7
|
- 0
|
8
8
|
- 0
|
9
|
-
-
|
10
|
-
version: 0.0.
|
9
|
+
- 3
|
10
|
+
version: 0.0.3
|
11
11
|
platform: ruby
|
12
12
|
authors:
|
13
13
|
- Patrick Debois
|
@@ -15,7 +15,7 @@ autorequire:
|
|
15
15
|
bindir: bin
|
16
16
|
cert_chain: []
|
17
17
|
|
18
|
-
date: 2012-
|
18
|
+
date: 2012-06-24 00:00:00 Z
|
19
19
|
dependencies:
|
20
20
|
- !ruby/object:Gem::Dependency
|
21
21
|
requirement: &id001 !ruby/object:Gem::Requirement
|
@@ -56,7 +56,7 @@ dependencies:
|
|
56
56
|
- 0
|
57
57
|
version: "0"
|
58
58
|
type: :runtime
|
59
|
-
name:
|
59
|
+
name: amqp
|
60
60
|
version_requirements: *id003
|
61
61
|
prerelease: false
|
62
62
|
- !ruby/object:Gem::Dependency
|
@@ -70,7 +70,7 @@ dependencies:
|
|
70
70
|
- 0
|
71
71
|
version: "0"
|
72
72
|
type: :runtime
|
73
|
-
name:
|
73
|
+
name: rack
|
74
74
|
version_requirements: *id004
|
75
75
|
prerelease: false
|
76
76
|
- !ruby/object:Gem::Dependency
|
@@ -84,7 +84,7 @@ dependencies:
|
|
84
84
|
- 0
|
85
85
|
version: "0"
|
86
86
|
type: :runtime
|
87
|
-
name:
|
87
|
+
name: yajl-ruby
|
88
88
|
version_requirements: *id005
|
89
89
|
prerelease: false
|
90
90
|
- !ruby/object:Gem::Dependency
|
@@ -98,11 +98,25 @@ dependencies:
|
|
98
98
|
- 0
|
99
99
|
version: "0"
|
100
100
|
type: :runtime
|
101
|
-
name:
|
101
|
+
name: fastercsv
|
102
102
|
version_requirements: *id006
|
103
103
|
prerelease: false
|
104
104
|
- !ruby/object:Gem::Dependency
|
105
105
|
requirement: &id007 !ruby/object:Gem::Requirement
|
106
|
+
none: false
|
107
|
+
requirements:
|
108
|
+
- - ">="
|
109
|
+
- !ruby/object:Gem::Version
|
110
|
+
hash: 3
|
111
|
+
segments:
|
112
|
+
- 0
|
113
|
+
version: "0"
|
114
|
+
type: :runtime
|
115
|
+
name: json
|
116
|
+
version_requirements: *id007
|
117
|
+
prerelease: false
|
118
|
+
- !ruby/object:Gem::Dependency
|
119
|
+
requirement: &id008 !ruby/object:Gem::Requirement
|
106
120
|
none: false
|
107
121
|
requirements:
|
108
122
|
- - ">="
|
@@ -115,9 +129,9 @@ dependencies:
|
|
115
129
|
version: 1.0.0
|
116
130
|
type: :development
|
117
131
|
name: bundler
|
118
|
-
version_requirements: *
|
132
|
+
version_requirements: *id008
|
119
133
|
prerelease: false
|
120
|
-
description: CLI
|
134
|
+
description: CLI interface to logstash
|
121
135
|
email:
|
122
136
|
- patrick.debois@jedi.be
|
123
137
|
executables:
|
@@ -135,8 +149,9 @@ files:
|
|
135
149
|
- bin/logstash-cli
|
136
150
|
- lib/logstash-cli.rb
|
137
151
|
- lib/logstash-cli/cli.rb
|
152
|
+
- lib/logstash-cli/command.rb
|
138
153
|
- lib/logstash-cli/command/grep.rb
|
139
|
-
- lib/logstash-cli/command/
|
154
|
+
- lib/logstash-cli/command/tail.rb
|
140
155
|
- lib/logstash-cli/version.rb
|
141
156
|
- logstashcli.gemspec
|
142
157
|
homepage: http://github.com/jedi4ever/logstash-cli/
|
@@ -1,139 +0,0 @@
|
|
1
|
-
#!/usr/bin/env ruby
|
2
|
-
# vim:filetype=ruby
|
3
|
-
require 'rubygems'
|
4
|
-
require 'bundler/setup'
|
5
|
-
begin
|
6
|
-
require 'tire'
|
7
|
-
require 'slop'
|
8
|
-
require 'time'
|
9
|
-
rescue LoadError
|
10
|
-
puts "You seem to be missing some key libraries"
|
11
|
-
puts "please run `gem install bundler --no-ri --no-rdoc; bundle install`"
|
12
|
-
end
|
13
|
-
|
14
|
-
begin
|
15
|
-
require 'yajl/json_gem'
|
16
|
-
rescue LoadError
|
17
|
-
puts "`gem install yajl-ruby for better performance"
|
18
|
-
end
|
19
|
-
|
20
|
-
opts = Slop.parse do
|
21
|
-
banner "Usage: search.rb -i <index> -f <facility>"
|
22
|
-
on :i, :index=, "index to search (default: logstash-#{Time.now.strftime('%Y.%m.%d')})", :default => "logstash-#{Time.now.strftime('%Y.%m.%d')}"
|
23
|
-
on :f, :facility=, "REQUIRED: Facility(application name) to use", nil
|
24
|
-
on :s, :size=, "number of results to return (default: 500)", true, :default => 500
|
25
|
-
on :c, :class_name=, "optional class name to narrow results by", nil
|
26
|
-
on :g, :grep=, "optional search on message content. Warning!!! This is slow", true
|
27
|
-
on :exceptions, "toggle exception search. Warning!!! This is slow", :default => false
|
28
|
-
on :live, "runs in live logging mode. This is A LOT of output. Please use non-wildcard facilities", :default => false
|
29
|
-
on :fields=, Array, "optional comma-separated list of fields to display (tstamp, msg, file, class_name, service) in order (default: tstamp,service,msg)"
|
30
|
-
on :h, :help, 'Print this help message', :tail => true do
|
31
|
-
#puts help
|
32
|
-
puts <<-EOF
|
33
|
-
|
34
|
-
--------
|
35
|
-
Examples
|
36
|
-
--------
|
37
|
-
- last 10 results log entries from curation including timestamp, classname and message:
|
38
|
-
|
39
|
-
search.rb -f curation* -s 10 --fields tstamp,class_name,msg
|
40
|
-
|
41
|
-
- last 5 entries for class name com.va (note the quote around * in the -f option):
|
42
|
-
|
43
|
-
search.rb -f "*" -s 5 -c com.va.*
|
44
|
-
|
45
|
-
- last 20 entries everywhere with timestamp, service name and message
|
46
|
-
|
47
|
-
search.rb -f "*" -s 20 --fields tstamp,service,msg
|
48
|
-
|
49
|
-
- last 5 exceptions everywhere
|
50
|
-
|
51
|
-
search.rb -f "*" -s 5 --exceptions
|
52
|
-
|
53
|
-
- live tail tracker_web
|
54
|
-
|
55
|
-
search.rb -f tracker_web --live
|
56
|
-
|
57
|
-
- live tail foo with custom display
|
58
|
-
|
59
|
-
search.rb -f foo --live --fields tstamp,service,class_name,msg
|
60
|
-
|
61
|
-
EOF
|
62
|
-
exit
|
63
|
-
end
|
64
|
-
end
|
65
|
-
|
66
|
-
if opts[:live]
|
67
|
-
require 'amqp'
|
68
|
-
require 'yajl/json_gem'
|
69
|
-
#https://github.com/ruby-amqp/amqp/pull/74
|
70
|
-
#URL naming scheme
|
71
|
-
AMQP.start("amqp://logstash:bar@localhost:7777") do |connection, open_ok|
|
72
|
-
channel = AMQP::Channel.new(connection, :auto_recovery => true)
|
73
|
-
exchange_name = "rawlogs"
|
74
|
-
|
75
|
-
channel.queue("", :auto_delete => true, :durable => false) do |queue, declare_ok|
|
76
|
-
queue.bind(exchange_name, :routing_key => opts[:facility])
|
77
|
-
queue.subscribe do |payload|
|
78
|
-
parsed_message = JSON.parse(payload)
|
79
|
-
require 'pp'
|
80
|
-
pp parsed_message
|
81
|
-
service = parsed_message["@fields"]["facility"]
|
82
|
-
class_name = parsed_message["@fields"]["_logger"]
|
83
|
-
file = parsed_message["@fields"]["file"]
|
84
|
-
msg = parsed_message["@message"]
|
85
|
-
#msg = parsed_message["@fields"]["full_message"]
|
86
|
-
tstamp = Time.iso8601(parsed_message["@timestamp"]).localtime.iso8601
|
87
|
-
fields = opts[:fields] || ["tstamp", "service", "msg"]
|
88
|
-
vals = fields.map {|x| x == fields[0] ? "\e[1m[#{eval(x)}]\e[0m" : eval(x)}
|
89
|
-
display = vals.join(" - ")
|
90
|
-
puts display
|
91
|
-
end
|
92
|
-
end
|
93
|
-
|
94
|
-
trap("INT") { puts "Shutting down..."; connection.close { EM.stop };exit }
|
95
|
-
end
|
96
|
-
end
|
97
|
-
|
98
|
-
if opts[:facility].nil?
|
99
|
-
puts "Facility (matches name of service) CANNOT be empty!"
|
100
|
-
require 'pp'
|
101
|
-
pp opts
|
102
|
-
exit
|
103
|
-
end
|
104
|
-
|
105
|
-
begin
|
106
|
-
Tire.configure {url "http://localhost:9200"}
|
107
|
-
puts opts[:index]
|
108
|
-
search = Tire.search(opts[:index]) do
|
109
|
-
query do
|
110
|
-
boolean do
|
111
|
-
must { string 'HOSTNAME:shipper'}
|
112
|
-
#must { string "facility:#{opts[:facility]}" } unless opts[:facility].nil?
|
113
|
-
#must { string "_logger:#{opts[:class_name]}" } unless opts[:class_name].nil?
|
114
|
-
#must { string "full_message:Exception*" } if opts[:exceptions]
|
115
|
-
must { string "message:#{opts[:grep]}*" } if opts[:grep]
|
116
|
-
end
|
117
|
-
end
|
118
|
-
sort do
|
119
|
-
by :@timestamp, 'desc'
|
120
|
-
end
|
121
|
-
size opts[:size]
|
122
|
-
end
|
123
|
-
rescue Exception => e
|
124
|
-
puts "\nSomething went wrong with the search. This is usually do to lucene query parsing of the 'grep' option"
|
125
|
-
exit
|
126
|
-
end
|
127
|
-
|
128
|
-
search.results.sort {|a,b| a[:@timestamp] <=> b[:@timestamp] }.each do |res|
|
129
|
-
service = res[:@fields][:facility]
|
130
|
-
class_name = res[:@fields][:_logger]
|
131
|
-
file = res[:@fields][:file]
|
132
|
-
#msg = res[:@fields][:full_message]
|
133
|
-
msg = res[:@fields][:message]
|
134
|
-
tstamp = Time.iso8601(res[:@timestamp]).localtime.iso8601
|
135
|
-
fields = opts[:fields] || ["tstamp", "service", "msg"]
|
136
|
-
vals = fields.map {|x| x == fields[0] ? "\e[1m[#{eval(x)}]\e[0m" : eval(x)}
|
137
|
-
display = vals.join(" - ")
|
138
|
-
puts display
|
139
|
-
end
|