logstash-core 2.3.4.snapshot1-java → 2.4.0-java
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.
Potentially problematic release.
This version of logstash-core might be problematic. Click here for more details.
- checksums.yaml +4 -4
- data/lib/logstash-core/version.rb +1 -1
- data/lib/logstash/agent.rb +28 -10
- data/lib/logstash/codecs/base.rb +29 -1
- data/lib/logstash/config/mixin.rb +62 -23
- data/lib/logstash/inputs/base.rb +1 -1
- data/lib/logstash/instrument/null_metric.rb +45 -0
- data/lib/logstash/logging/json.rb +21 -0
- data/lib/logstash/output_delegator.rb +3 -3
- data/lib/logstash/outputs/base.rb +32 -1
- data/lib/logstash/pipeline.rb +1 -1
- data/lib/logstash/plugin.rb +11 -1
- data/lib/logstash/util/safe_uri.rb +50 -0
- data/lib/logstash/version.rb +1 -1
- data/locales/en.yml +4 -0
- data/logstash-core.gemspec +3 -2
- data/spec/logstash/agent_spec.rb +8 -3
- data/spec/logstash/codecs/base_spec.rb +74 -0
- data/spec/logstash/config/mixin_spec.rb +157 -0
- data/spec/logstash/instrument/null_metric_spec.rb +63 -0
- data/spec/logstash/output_delegator_spec.rb +1 -0
- data/spec/logstash/outputs/base_spec.rb +107 -0
- data/spec/logstash/plugin_spec.rb +29 -3
- data/spec/logstash/runner_spec.rb +27 -0
- data/spec/logstash/shutdown_watcher_spec.rb +1 -0
- metadata +15 -8
data/lib/logstash/pipeline.rb
CHANGED
@@ -392,7 +392,7 @@ module LogStash; class Pipeline
|
|
392
392
|
def shutdown_workers
|
393
393
|
# Each worker thread will receive this exactly once!
|
394
394
|
@worker_threads.each do |t|
|
395
|
-
@logger.debug("Pushing shutdown", :thread => t)
|
395
|
+
@logger.debug("Pushing shutdown", :thread => t.inspect)
|
396
396
|
@input_queue.push(LogStash::SHUTDOWN)
|
397
397
|
end
|
398
398
|
|
data/lib/logstash/plugin.rb
CHANGED
@@ -2,6 +2,7 @@
|
|
2
2
|
require "logstash/namespace"
|
3
3
|
require "logstash/logging"
|
4
4
|
require "logstash/config/mixin"
|
5
|
+
require "logstash/instrument/null_metric"
|
5
6
|
require "cabin"
|
6
7
|
require "concurrent"
|
7
8
|
|
@@ -26,13 +27,14 @@ class LogStash::Plugin
|
|
26
27
|
def initialize(params=nil)
|
27
28
|
@params = LogStash::Util.deep_clone(params)
|
28
29
|
@logger = Cabin::Channel.get(LogStash)
|
30
|
+
@metric_plugin = LogStash::Instrument::NullMetric.new
|
29
31
|
end
|
30
32
|
|
31
33
|
# close is called during shutdown, after the plugin worker
|
32
34
|
# main task terminates
|
33
35
|
public
|
34
36
|
def do_close
|
35
|
-
@logger.debug("closing", :plugin => self)
|
37
|
+
@logger.debug("closing", :plugin => self.class.name)
|
36
38
|
close
|
37
39
|
end
|
38
40
|
|
@@ -47,6 +49,14 @@ class LogStash::Plugin
|
|
47
49
|
return "#{self.class.name}: #{@params}"
|
48
50
|
end
|
49
51
|
|
52
|
+
# This is a shim to make sure that plugin
|
53
|
+
# that record metric still work with 2.4
|
54
|
+
#
|
55
|
+
# https://github.com/elastic/logstash/issues/5539
|
56
|
+
def metric
|
57
|
+
@metric_plugin
|
58
|
+
end
|
59
|
+
|
50
60
|
public
|
51
61
|
def inspect
|
52
62
|
if !@params.nil?
|
@@ -0,0 +1,50 @@
|
|
1
|
+
# encoding: utf-8
|
2
|
+
require "logstash/namespace"
|
3
|
+
require "logstash/util"
|
4
|
+
|
5
|
+
# This class exists to quietly wrap a password string so that, when printed or
|
6
|
+
# logged, you don't accidentally print the password itself.
|
7
|
+
class LogStash::Util::SafeURI
|
8
|
+
PASS_PLACEHOLDER = "xxxxxx".freeze
|
9
|
+
HOSTNAME_PORT_REGEX=/\A(?<hostname>([A-Za-z0-9\.\-]+)|\[[0-9A-Fa-f\:]+\])(:(?<port>\d+))?\Z/
|
10
|
+
|
11
|
+
extend Forwardable
|
12
|
+
|
13
|
+
def_delegators :@uri, :coerce, :query=, :route_from, :port=, :default_port, :select, :normalize!, :absolute?, :registry=, :path, :password, :hostname, :merge, :normalize, :host, :component_ary, :userinfo=, :query, :set_opaque, :+, :merge!, :-, :password=, :parser, :port, :set_host, :set_path, :opaque=, :scheme, :fragment=, :set_query, :set_fragment, :userinfo, :hostname=, :set_port, :path=, :registry, :opaque, :route_to, :set_password, :hierarchical?, :set_user, :set_registry, :set_userinfo, :fragment, :component, :user=, :set_scheme, :absolute, :host=, :relative?, :scheme=, :user
|
14
|
+
|
15
|
+
attr_reader :uri
|
16
|
+
|
17
|
+
public
|
18
|
+
def initialize(arg)
|
19
|
+
@uri = case arg
|
20
|
+
when String
|
21
|
+
arg = "//#{arg}" if HOSTNAME_PORT_REGEX.match(arg)
|
22
|
+
URI.parse(arg)
|
23
|
+
when URI
|
24
|
+
arg
|
25
|
+
else
|
26
|
+
raise ArgumentError, "Expected a string or URI, got a #{arg.class} creating a URL"
|
27
|
+
end
|
28
|
+
end
|
29
|
+
|
30
|
+
def to_s
|
31
|
+
sanitized.to_s
|
32
|
+
end
|
33
|
+
|
34
|
+
def inspect
|
35
|
+
sanitized.to_s
|
36
|
+
end
|
37
|
+
|
38
|
+
def sanitized
|
39
|
+
return uri unless uri.password # nothing to sanitize here!
|
40
|
+
|
41
|
+
safe = uri.clone
|
42
|
+
safe.password = PASS_PLACEHOLDER
|
43
|
+
safe
|
44
|
+
end
|
45
|
+
|
46
|
+
def ==(other)
|
47
|
+
other.is_a?(::LogStash::Util::SafeURI) ? @uri == other.uri : false
|
48
|
+
end
|
49
|
+
end
|
50
|
+
|
data/lib/logstash/version.rb
CHANGED
data/locales/en.yml
CHANGED
@@ -225,3 +225,7 @@ en:
|
|
225
225
|
Print the compiled config ruby code out as a debug log (you must also have --debug enabled).
|
226
226
|
WARNING: This will include any 'password' options passed to plugin configs as plaintext, and may result
|
227
227
|
in plaintext passwords appearing in your logs!
|
228
|
+
log-in-json: |+
|
229
|
+
Specify that Logstash should write its own logs in JSON form - one
|
230
|
+
event per line. If false, Logstash will log using Ruby's
|
231
|
+
Object#inspect (not easy to machine-parse)
|
data/logstash-core.gemspec
CHANGED
@@ -17,7 +17,8 @@ Gem::Specification.new do |gem|
|
|
17
17
|
gem.require_paths = ["lib"]
|
18
18
|
gem.version = LOGSTASH_CORE_VERSION
|
19
19
|
|
20
|
-
gem.add_runtime_dependency "logstash-core-event", "2.
|
20
|
+
gem.add_runtime_dependency "logstash-core-event", "2.4.0"
|
21
|
+
# gem.add_runtime_dependency "logstash-core-event-java", "2.4.0.dev"
|
21
22
|
|
22
23
|
gem.add_runtime_dependency "cabin", "~> 0.8.0" #(Apache 2.0 license)
|
23
24
|
gem.add_runtime_dependency "pry", "~> 0.10.1" #(Ruby license)
|
@@ -26,7 +27,7 @@ Gem::Specification.new do |gem|
|
|
26
27
|
gem.add_runtime_dependency "filesize", "0.0.4" #(MIT license) for :bytes config validator
|
27
28
|
gem.add_runtime_dependency "gems", "~> 0.8.3" #(MIT license)
|
28
29
|
gem.add_runtime_dependency "concurrent-ruby", "0.9.2"
|
29
|
-
gem.add_runtime_dependency "jruby-openssl", "0.9.
|
30
|
+
gem.add_runtime_dependency "jruby-openssl", "0.9.16" # >= 0.9.13 Required to support TLSv1.2
|
30
31
|
|
31
32
|
# TODO(sissel): Treetop 1.5.x doesn't seem to work well, but I haven't
|
32
33
|
# investigated what the cause might be. -Jordan
|
data/spec/logstash/agent_spec.rb
CHANGED
@@ -255,7 +255,7 @@ describe LogStash::Agent do
|
|
255
255
|
it "should fail with single invalid dir path" do
|
256
256
|
expect(File).to receive(:directory?).and_return(false)
|
257
257
|
expect(LogStash::Environment).not_to receive(:add_plugin_path)
|
258
|
-
expect{subject.configure_plugin_paths(single_path)}.to raise_error(
|
258
|
+
expect{subject.configure_plugin_paths(single_path)}.to raise_error(Clamp::UsageError)
|
259
259
|
end
|
260
260
|
|
261
261
|
it "should add multiple valid dir path to the environment" do
|
@@ -293,15 +293,15 @@ describe LogStash::Agent do
|
|
293
293
|
it "should fail with single invalid dir path" do
|
294
294
|
expect(File).to receive(:directory?).and_return(false)
|
295
295
|
expect(LogStash::Environment).not_to receive(:add_plugin_path)
|
296
|
-
expect{subject.configure_plugin_paths(single_path)}.to raise_error(
|
296
|
+
expect{subject.configure_plugin_paths(single_path)}.to raise_error(Clamp::UsageError)
|
297
297
|
end
|
298
298
|
end
|
299
299
|
|
300
300
|
describe "--config-test" do
|
301
301
|
let(:cli_args) { ["-t", "-e", pipeline_string] }
|
302
|
+
let(:pipeline_string) { "input { } filter { } output { }" }
|
302
303
|
|
303
304
|
context "with a good configuration" do
|
304
|
-
let(:pipeline_string) { "input { } filter { } output { }" }
|
305
305
|
it "should exit successfuly" do
|
306
306
|
expect(subject.run(cli_args)).to eq(0)
|
307
307
|
end
|
@@ -313,6 +313,11 @@ describe LogStash::Agent do
|
|
313
313
|
expect(subject.run(cli_args)).to eq(1)
|
314
314
|
end
|
315
315
|
end
|
316
|
+
|
317
|
+
it "requests the config loader to format_config" do
|
318
|
+
expect(subject.config_loader).to receive(:format_config)
|
319
|
+
subject.run(cli_args)
|
320
|
+
end
|
316
321
|
end
|
317
322
|
|
318
323
|
describe "pipeline settings" do
|
@@ -0,0 +1,74 @@
|
|
1
|
+
# encoding: utf-8
|
2
|
+
require "spec_helper"
|
3
|
+
|
4
|
+
DATA_DOUBLE = "data".freeze
|
5
|
+
|
6
|
+
# use a dummy NOOP output to test Outputs::Base
|
7
|
+
class LogStash::Codecs::NOOPAsync < LogStash::Codecs::Base
|
8
|
+
attr_reader :last_result
|
9
|
+
config_name "noop_async"
|
10
|
+
|
11
|
+
def encode(event)
|
12
|
+
@last_result = @on_event.call(event, DATA_DOUBLE)
|
13
|
+
end
|
14
|
+
end
|
15
|
+
|
16
|
+
class LogStash::Codecs::NOOPSync < LogStash::Codecs::Base
|
17
|
+
attr_reader :last_result
|
18
|
+
config_name "noop_sync"
|
19
|
+
|
20
|
+
def encode_sync(event)
|
21
|
+
DATA_DOUBLE
|
22
|
+
end
|
23
|
+
end
|
24
|
+
|
25
|
+
class LogStash::Codecs::NOOPMulti < LogStash::Codecs::Base
|
26
|
+
attr_reader :last_result
|
27
|
+
config_name "noop_multi"
|
28
|
+
|
29
|
+
def encode_sync(event)
|
30
|
+
DATA_DOUBLE
|
31
|
+
end
|
32
|
+
end
|
33
|
+
|
34
|
+
describe LogStash::Codecs::Base do
|
35
|
+
let(:params) { {} }
|
36
|
+
subject(:instance) { klass.new(params.dup) }
|
37
|
+
let(:event) { double("event") }
|
38
|
+
let(:encoded_data) { DATA_DOUBLE }
|
39
|
+
let(:encoded_tuple) { [event, encoded_data] }
|
40
|
+
|
41
|
+
describe "encoding" do
|
42
|
+
shared_examples "encoder types" do |codec_class|
|
43
|
+
let(:klass) { codec_class }
|
44
|
+
|
45
|
+
describe "#{codec_class}" do
|
46
|
+
describe "multi_encode" do
|
47
|
+
it "should return an array of [event,data] tuples" do
|
48
|
+
expect(instance.multi_encode([event,event])).to eq([encoded_tuple, encoded_tuple])
|
49
|
+
end
|
50
|
+
end
|
51
|
+
|
52
|
+
describe "#encode" do
|
53
|
+
before do
|
54
|
+
@result = nil
|
55
|
+
instance.on_event do |event, data|
|
56
|
+
@result = [event, data]
|
57
|
+
end
|
58
|
+
instance.encode(event)
|
59
|
+
end
|
60
|
+
|
61
|
+
it "should yield the correct result" do
|
62
|
+
expect(@result).to eq(encoded_tuple)
|
63
|
+
end
|
64
|
+
end
|
65
|
+
end
|
66
|
+
end
|
67
|
+
|
68
|
+
include_examples("encoder types", LogStash::Codecs::NOOPAsync)
|
69
|
+
include_examples("encoder types", LogStash::Codecs::NOOPSync)
|
70
|
+
include_examples("encoder types", LogStash::Codecs::NOOPMulti)
|
71
|
+
end
|
72
|
+
end
|
73
|
+
|
74
|
+
|
@@ -68,6 +68,74 @@ describe LogStash::Config::Mixin do
|
|
68
68
|
end
|
69
69
|
end
|
70
70
|
|
71
|
+
context "when validating lists of items" do
|
72
|
+
let(:klass) do
|
73
|
+
Class.new(LogStash::Filters::Base) do
|
74
|
+
config_name "multiuri"
|
75
|
+
config :uris, :validate => :uri, :list => true
|
76
|
+
config :strings, :validate => :string, :list => true
|
77
|
+
config :required_strings, :validate => :string, :list => true, :required => true
|
78
|
+
end
|
79
|
+
end
|
80
|
+
|
81
|
+
let(:uris) { ["http://example.net/1", "http://example.net/2"] }
|
82
|
+
let(:safe_uris) { uris.map {|str| ::LogStash::Util::SafeURI.new(str) } }
|
83
|
+
let(:strings) { ["I am a", "modern major general"] }
|
84
|
+
let(:required_strings) { ["required", "strings"] }
|
85
|
+
|
86
|
+
subject { klass.new("uris" => uris, "strings" => strings, "required_strings" => required_strings) }
|
87
|
+
|
88
|
+
it "a URI list should return an array of URIs" do
|
89
|
+
expect(subject.uris).to match_array(safe_uris)
|
90
|
+
end
|
91
|
+
|
92
|
+
it "a string list should return an array of strings" do
|
93
|
+
expect(subject.strings).to match_array(strings)
|
94
|
+
end
|
95
|
+
|
96
|
+
context "with a scalar value" do
|
97
|
+
let(:strings) { "foo" }
|
98
|
+
|
99
|
+
it "should return the scalar value as a single element array" do
|
100
|
+
expect(subject.strings).to match_array([strings])
|
101
|
+
end
|
102
|
+
end
|
103
|
+
|
104
|
+
context "with an empty list" do
|
105
|
+
let(:strings) { [] }
|
106
|
+
|
107
|
+
it "should return nil" do
|
108
|
+
expect(subject.strings).to be_nil
|
109
|
+
end
|
110
|
+
end
|
111
|
+
|
112
|
+
describe "with required => true" do
|
113
|
+
context "and a single element" do
|
114
|
+
let(:required_strings) { ["foo"] }
|
115
|
+
|
116
|
+
it "should return the single value" do
|
117
|
+
expect(subject.required_strings).to eql(required_strings)
|
118
|
+
end
|
119
|
+
end
|
120
|
+
|
121
|
+
context "with an empty list" do
|
122
|
+
let (:required_strings) { [] }
|
123
|
+
|
124
|
+
it "should raise a configuration error" do
|
125
|
+
expect { subject.required_strings }.to raise_error(LogStash::ConfigurationError)
|
126
|
+
end
|
127
|
+
end
|
128
|
+
|
129
|
+
context "with no value specified" do
|
130
|
+
let (:required_strings) { nil }
|
131
|
+
|
132
|
+
it "should raise a configuration error" do
|
133
|
+
expect { subject.required_strings }.to raise_error(LogStash::ConfigurationError)
|
134
|
+
end
|
135
|
+
end
|
136
|
+
end
|
137
|
+
end
|
138
|
+
|
71
139
|
context "when validating :password" do
|
72
140
|
let(:klass) do
|
73
141
|
Class.new(LogStash::Filters::Base) do
|
@@ -102,6 +170,95 @@ describe LogStash::Config::Mixin do
|
|
102
170
|
end
|
103
171
|
end
|
104
172
|
|
173
|
+
context "when validating :uri" do
|
174
|
+
let(:klass) do
|
175
|
+
Class.new(LogStash::Filters::Base) do
|
176
|
+
config_name "fakeuri"
|
177
|
+
config :uri, :validate => :uri
|
178
|
+
end
|
179
|
+
end
|
180
|
+
|
181
|
+
shared_examples("safe URI") do |options|
|
182
|
+
options ||= {}
|
183
|
+
|
184
|
+
subject { klass.new("uri" => uri_str) }
|
185
|
+
|
186
|
+
it "should be a SafeURI object" do
|
187
|
+
expect(subject.uri).to(be_a(LogStash::Util::SafeURI))
|
188
|
+
end
|
189
|
+
|
190
|
+
it "should correctly copy URI types" do
|
191
|
+
clone = subject.class.new(subject.params)
|
192
|
+
expect(clone.uri.to_s).to eql(uri_hidden)
|
193
|
+
end
|
194
|
+
|
195
|
+
it "should make the real URI object availale under #uri" do
|
196
|
+
expect(subject.uri.uri).to be_a(::URI)
|
197
|
+
end
|
198
|
+
|
199
|
+
it "should obfuscate original_params" do
|
200
|
+
expect(subject.original_params['uri']).to(be_a(LogStash::Util::SafeURI))
|
201
|
+
end
|
202
|
+
|
203
|
+
if !options[:exclude_password_specs]
|
204
|
+
describe "passwords" do
|
205
|
+
it "should make password values hidden with #to_s" do
|
206
|
+
expect(subject.uri.to_s).to eql(uri_hidden)
|
207
|
+
end
|
208
|
+
|
209
|
+
it "should make password values hidden with #inspect" do
|
210
|
+
expect(subject.uri.inspect).to eql(uri_hidden)
|
211
|
+
end
|
212
|
+
end
|
213
|
+
end
|
214
|
+
|
215
|
+
context "attributes" do
|
216
|
+
[:scheme, :user, :password, :hostname, :path].each do |attr|
|
217
|
+
it "should make #{attr} available" do
|
218
|
+
expect(subject.uri.send(attr)).to eql(self.send(attr))
|
219
|
+
end
|
220
|
+
end
|
221
|
+
end
|
222
|
+
end
|
223
|
+
|
224
|
+
context "with a host:port combination" do
|
225
|
+
let(:scheme) { nil }
|
226
|
+
let(:user) { nil }
|
227
|
+
let(:password) { nil }
|
228
|
+
let(:hostname) { "myhostname" }
|
229
|
+
let(:port) { 1234 }
|
230
|
+
let(:path) { "" }
|
231
|
+
let(:uri_str) { "#{hostname}:#{port}" }
|
232
|
+
let(:uri_hidden) { "//#{hostname}:#{port}" }
|
233
|
+
|
234
|
+
include_examples("safe URI", :exclude_password_specs => true)
|
235
|
+
end
|
236
|
+
|
237
|
+
context "with a username / password" do
|
238
|
+
let(:scheme) { "myscheme" }
|
239
|
+
let(:user) { "myuser" }
|
240
|
+
let(:password) { "fancypants" }
|
241
|
+
let(:hostname) { "myhostname" }
|
242
|
+
let(:path) { "/my/path" }
|
243
|
+
let(:uri_str) { "#{scheme}://#{user}:#{password}@#{hostname}#{path}" }
|
244
|
+
let(:uri_hidden) { "#{scheme}://#{user}:#{LogStash::Util::SafeURI::PASS_PLACEHOLDER}@#{hostname}#{path}" }
|
245
|
+
|
246
|
+
include_examples("safe URI")
|
247
|
+
end
|
248
|
+
|
249
|
+
context "without a username / password" do
|
250
|
+
let(:scheme) { "myscheme" }
|
251
|
+
let(:user) { nil }
|
252
|
+
let(:password) { nil }
|
253
|
+
let(:hostname) { "myhostname" }
|
254
|
+
let(:path) { "/my/path" }
|
255
|
+
let(:uri_str) { "#{scheme}://#{hostname}#{path}" }
|
256
|
+
let(:uri_hidden) { "#{scheme}://#{hostname}#{path}" }
|
257
|
+
|
258
|
+
include_examples("safe URI")
|
259
|
+
end
|
260
|
+
end
|
261
|
+
|
105
262
|
describe "obsolete settings" do
|
106
263
|
let(:plugin_class) do
|
107
264
|
Class.new(LogStash::Inputs::Base) do
|
@@ -0,0 +1,63 @@
|
|
1
|
+
# encoding: utf-8
|
2
|
+
require "logstash/instrument/null_metric"
|
3
|
+
|
4
|
+
describe LogStash::Instrument::NullMetric do
|
5
|
+
let(:key) { "galaxy" }
|
6
|
+
|
7
|
+
describe "#increment" do
|
8
|
+
it "allows to increment a key with no amount" do
|
9
|
+
expect { subject.increment(key, 100) }.not_to raise_error
|
10
|
+
end
|
11
|
+
|
12
|
+
it "allow to increment a key" do
|
13
|
+
expect { subject.increment(key) }.not_to raise_error
|
14
|
+
end
|
15
|
+
end
|
16
|
+
|
17
|
+
describe "#decrement" do
|
18
|
+
it "allows to decrement a key with no amount" do
|
19
|
+
expect { subject.decrement(key, 100) }.not_to raise_error
|
20
|
+
end
|
21
|
+
|
22
|
+
it "allow to decrement a key" do
|
23
|
+
expect { subject.decrement(key) }.not_to raise_error
|
24
|
+
end
|
25
|
+
end
|
26
|
+
|
27
|
+
describe "#gauge" do
|
28
|
+
it "allows to set a value" do
|
29
|
+
expect { subject.gauge(key, "pluto") }.not_to raise_error
|
30
|
+
end
|
31
|
+
end
|
32
|
+
|
33
|
+
describe "#report_time" do
|
34
|
+
it "allow to record time" do
|
35
|
+
expect { subject.report_time(key, 1000) }.not_to raise_error
|
36
|
+
end
|
37
|
+
end
|
38
|
+
|
39
|
+
describe "#time" do
|
40
|
+
it "allow to record time with a block given" do
|
41
|
+
expect do
|
42
|
+
subject.time(key) { 1+1 }
|
43
|
+
end.not_to raise_error
|
44
|
+
end
|
45
|
+
|
46
|
+
it "when using a block it return the generated value" do
|
47
|
+
expect(subject.time(key) { 1+1 }).to eq(2)
|
48
|
+
end
|
49
|
+
|
50
|
+
it "allow to record time with no block given" do
|
51
|
+
expect do
|
52
|
+
clock = subject.time(key)
|
53
|
+
clock.stop
|
54
|
+
end.not_to raise_error
|
55
|
+
end
|
56
|
+
end
|
57
|
+
|
58
|
+
describe "#namespace" do
|
59
|
+
it "return a NullMetric" do
|
60
|
+
expect(subject.namespace(key)).to be_kind_of LogStash::Instrument::NullMetric
|
61
|
+
end
|
62
|
+
end
|
63
|
+
end
|