logstash-core 5.0.0.alpha5.snapshot1-java → 5.0.0.alpha6.snapshot1-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 +1 -1
- data/lib/logstash/api/commands/default_metadata.rb +1 -1
- data/lib/logstash/api/commands/hot_threads_reporter.rb +4 -7
- data/lib/logstash/api/commands/node.rb +5 -4
- data/lib/logstash/api/commands/stats.rb +8 -3
- data/lib/logstash/api/modules/base.rb +5 -0
- data/lib/logstash/api/modules/node.rb +1 -2
- data/lib/logstash/api/modules/node_stats.rb +1 -2
- data/lib/logstash/codecs/base.rb +29 -1
- data/lib/logstash/config/mixin.rb +1 -1
- data/lib/logstash/environment.rb +5 -5
- data/lib/logstash/filter_delegator.rb +4 -5
- data/lib/logstash/instrument/periodic_poller/jvm.rb +43 -10
- data/lib/logstash/output_delegator.rb +33 -168
- data/lib/logstash/output_delegator_strategies/legacy.rb +29 -0
- data/lib/logstash/output_delegator_strategies/shared.rb +20 -0
- data/lib/logstash/output_delegator_strategies/single.rb +23 -0
- data/lib/logstash/output_delegator_strategy_registry.rb +36 -0
- data/lib/logstash/outputs/base.rb +39 -26
- data/lib/logstash/patches/clamp.rb +6 -0
- data/lib/logstash/pipeline.rb +42 -14
- data/lib/logstash/pipeline_reporter.rb +2 -8
- data/lib/logstash/plugin.rb +6 -10
- data/lib/logstash/runner.rb +12 -9
- data/lib/logstash/settings.rb +124 -21
- data/lib/logstash/util/wrapped_synchronous_queue.rb +17 -1
- data/lib/logstash/version.rb +1 -1
- data/lib/logstash/webserver.rb +44 -33
- data/locales/en.yml +5 -1
- data/logstash-core.gemspec +2 -2
- data/spec/api/lib/api/node_spec.rb +62 -10
- data/spec/api/lib/api/node_stats_spec.rb +16 -3
- data/spec/api/lib/api/support/resource_dsl_methods.rb +11 -1
- data/spec/api/spec_helper.rb +1 -1
- data/spec/conditionals_spec.rb +12 -1
- data/spec/logstash/agent_spec.rb +3 -0
- data/spec/logstash/codecs/base_spec.rb +74 -0
- data/spec/logstash/instrument/periodic_poller/jvm_spec.rb +37 -10
- data/spec/logstash/output_delegator_spec.rb +64 -89
- data/spec/logstash/outputs/base_spec.rb +91 -15
- data/spec/logstash/pipeline_reporter_spec.rb +1 -6
- data/spec/logstash/pipeline_spec.rb +20 -22
- data/spec/logstash/plugin_spec.rb +3 -3
- data/spec/logstash/runner_spec.rb +86 -3
- data/spec/logstash/settings/integer_spec.rb +20 -0
- data/spec/logstash/settings/numeric_spec.rb +28 -0
- data/spec/logstash/settings/port_range_spec.rb +93 -0
- data/spec/logstash/util/wrapped_synchronous_queue_spec.rb +6 -0
- data/spec/logstash/webserver_spec.rb +95 -0
- metadata +20 -6
data/lib/logstash/settings.rb
CHANGED
@@ -176,18 +176,43 @@ module LogStash
|
|
176
176
|
end
|
177
177
|
end
|
178
178
|
|
179
|
-
|
180
|
-
|
181
|
-
class Boolean < Setting
|
182
|
-
def initialize(name, default, strict=true, &validator_proc)
|
179
|
+
class Coercible < Setting
|
180
|
+
def initialize(name, klass, default=nil, strict=true, &validator_proc)
|
183
181
|
@name = name
|
184
|
-
|
182
|
+
unless klass.is_a?(Class)
|
183
|
+
raise ArgumentError.new("Setting \"#{@name}\" must be initialized with a class (received #{klass})")
|
184
|
+
end
|
185
|
+
@klass = klass
|
186
|
+
@validator_proc = validator_proc
|
185
187
|
@value = nil
|
186
188
|
@value_is_set = false
|
187
|
-
|
188
|
-
|
189
|
-
|
190
|
-
|
189
|
+
|
190
|
+
if strict
|
191
|
+
coerced_default = coerce(default)
|
192
|
+
validate(coerced_default)
|
193
|
+
@default = coerced_default
|
194
|
+
else
|
195
|
+
@default = default
|
196
|
+
end
|
197
|
+
end
|
198
|
+
|
199
|
+
def set(value)
|
200
|
+
coerced_value = coerce(value)
|
201
|
+
validate(coerced_value)
|
202
|
+
@value = coerce(coerced_value)
|
203
|
+
@value_is_set = true
|
204
|
+
@value
|
205
|
+
end
|
206
|
+
|
207
|
+
def coerce(value)
|
208
|
+
raise NotImplementedError.new("Please implement #coerce for #{self.class}")
|
209
|
+
end
|
210
|
+
end
|
211
|
+
### Specific settings #####
|
212
|
+
|
213
|
+
class Boolean < Coercible
|
214
|
+
def initialize(name, default, strict=true, &validator_proc)
|
215
|
+
super(name, Object, default, strict, &validator_proc)
|
191
216
|
end
|
192
217
|
|
193
218
|
def coerce(value)
|
@@ -200,25 +225,104 @@ module LogStash
|
|
200
225
|
raise ArgumentError.new("could not coerce #{value} into a boolean")
|
201
226
|
end
|
202
227
|
end
|
228
|
+
end
|
203
229
|
|
204
|
-
|
205
|
-
|
206
|
-
|
207
|
-
|
208
|
-
|
209
|
-
|
230
|
+
class Numeric < Coercible
|
231
|
+
def initialize(name, default=nil, strict=true)
|
232
|
+
super(name, ::Numeric, default, strict)
|
233
|
+
end
|
234
|
+
|
235
|
+
def coerce(v)
|
236
|
+
return v if v.is_a?(::Numeric)
|
237
|
+
|
238
|
+
# I hate these "exceptions as control flow" idioms
|
239
|
+
# but Ruby's `"a".to_i => 0` makes it hard to do anything else.
|
240
|
+
coerced_value = (Integer(v) rescue nil) || (Float(v) rescue nil)
|
241
|
+
|
242
|
+
if coerced_value.nil?
|
243
|
+
raise ArgumentError.new("Failed to coerce value to Numeric. Received #{v} (#{v.class})")
|
244
|
+
else
|
245
|
+
coerced_value
|
246
|
+
end
|
210
247
|
end
|
211
248
|
end
|
212
249
|
|
213
|
-
class
|
250
|
+
class Integer < Coercible
|
214
251
|
def initialize(name, default=nil, strict=true)
|
215
|
-
super(name, ::
|
252
|
+
super(name, ::Integer, default, strict)
|
253
|
+
end
|
254
|
+
|
255
|
+
def coerce(value)
|
256
|
+
return value unless value.is_a?(::String)
|
257
|
+
|
258
|
+
coerced_value = Integer(value) rescue nil
|
259
|
+
|
260
|
+
if coerced_value.nil?
|
261
|
+
raise ArgumentError.new("Failed to coerce value to Integer. Received #{value} (#{value.class})")
|
262
|
+
else
|
263
|
+
coerced_value
|
264
|
+
end
|
265
|
+
end
|
266
|
+
end
|
267
|
+
|
268
|
+
class PositiveInteger < Integer
|
269
|
+
def initialize(name, default=nil, strict=true)
|
270
|
+
super(name, default, strict) do |v|
|
271
|
+
if v > 0
|
272
|
+
true
|
273
|
+
else
|
274
|
+
raise ArgumentError.new("Number must be bigger than 0. Received: #{v}")
|
275
|
+
end
|
276
|
+
end
|
216
277
|
end
|
217
278
|
end
|
218
279
|
|
219
|
-
class Port <
|
280
|
+
class Port < Integer
|
281
|
+
VALID_PORT_RANGE = 1..65535
|
282
|
+
|
220
283
|
def initialize(name, default=nil, strict=true)
|
221
|
-
super(name,
|
284
|
+
super(name, default, strict) { |value| valid?(value) }
|
285
|
+
end
|
286
|
+
|
287
|
+
def valid?(port)
|
288
|
+
VALID_PORT_RANGE.cover?(port)
|
289
|
+
end
|
290
|
+
end
|
291
|
+
|
292
|
+
class PortRange < Coercible
|
293
|
+
PORT_SEPARATOR = "-"
|
294
|
+
|
295
|
+
def initialize(name, default=nil, strict=true)
|
296
|
+
super(name, ::Range, default, strict=true) { |value| valid?(value) }
|
297
|
+
end
|
298
|
+
|
299
|
+
def valid?(range)
|
300
|
+
Port::VALID_PORT_RANGE.first <= range.first && Port::VALID_PORT_RANGE.last >= range.last
|
301
|
+
end
|
302
|
+
|
303
|
+
def coerce(value)
|
304
|
+
case value
|
305
|
+
when ::Range
|
306
|
+
value
|
307
|
+
when ::Fixnum
|
308
|
+
value..value
|
309
|
+
when ::String
|
310
|
+
first, last = value.split(PORT_SEPARATOR)
|
311
|
+
last = first if last.nil?
|
312
|
+
begin
|
313
|
+
(Integer(first))..(Integer(last))
|
314
|
+
rescue ArgumentError # Trap and reraise a more human error
|
315
|
+
raise ArgumentError.new("Could not coerce #{value} into a port range")
|
316
|
+
end
|
317
|
+
else
|
318
|
+
raise ArgumentError.new("Could not coerce #{value} into a port range")
|
319
|
+
end
|
320
|
+
end
|
321
|
+
|
322
|
+
def validate(value)
|
323
|
+
unless valid?(value)
|
324
|
+
raise ArgumentError.new("Invalid value \"#{value}, valid options are within the range of #{Port::VALID_PORT_RANGE.first}-#{Port::VALID_PORT_RANGE.last}")
|
325
|
+
end
|
222
326
|
end
|
223
327
|
end
|
224
328
|
|
@@ -242,7 +346,7 @@ module LogStash
|
|
242
346
|
def validate(value)
|
243
347
|
super(value)
|
244
348
|
unless @possible_strings.empty? || @possible_strings.include?(value)
|
245
|
-
raise ArgumentError.new("
|
349
|
+
raise ArgumentError.new("Invalid value \"#{value}\". Options are: #{@possible_strings.inspect}")
|
246
350
|
end
|
247
351
|
end
|
248
352
|
end
|
@@ -270,7 +374,6 @@ module LogStash
|
|
270
374
|
end
|
271
375
|
end
|
272
376
|
end
|
273
|
-
|
274
377
|
end
|
275
378
|
|
276
379
|
SETTINGS = Settings.new
|
@@ -57,6 +57,9 @@ module LogStash; module Util
|
|
57
57
|
# Note that @infilght_batches as a central mechanism for tracking inflight
|
58
58
|
# batches will fail if we have multiple read clients in the pipeline.
|
59
59
|
@inflight_batches = {}
|
60
|
+
|
61
|
+
# allow the worker thread to report the execution time of the filter + output
|
62
|
+
@inflight_clocks = {}
|
60
63
|
@batch_size = batch_size
|
61
64
|
@wait_for = wait_for
|
62
65
|
end
|
@@ -89,6 +92,7 @@ module LogStash; module Util
|
|
89
92
|
batch = ReadBatch.new(@queue, @batch_size, @wait_for)
|
90
93
|
add_starting_metrics(batch)
|
91
94
|
set_current_thread_inflight_batch(batch)
|
95
|
+
start_clock
|
92
96
|
batch
|
93
97
|
end
|
94
98
|
end
|
@@ -100,11 +104,23 @@ module LogStash; module Util
|
|
100
104
|
def close_batch(batch)
|
101
105
|
@mutex.synchronize do
|
102
106
|
@inflight_batches.delete(Thread.current)
|
107
|
+
stop_clock
|
103
108
|
end
|
104
109
|
end
|
105
110
|
|
111
|
+
def start_clock
|
112
|
+
@inflight_clocks[Thread.current] = [
|
113
|
+
@event_metric.time(:duration_in_millis),
|
114
|
+
@pipeline_metric.time(:duration_in_millis)
|
115
|
+
]
|
116
|
+
end
|
117
|
+
|
118
|
+
def stop_clock
|
119
|
+
@inflight_clocks[Thread.current].each(&:stop)
|
120
|
+
@inflight_clocks.delete(Thread.current)
|
121
|
+
end
|
122
|
+
|
106
123
|
def add_starting_metrics(batch)
|
107
|
-
return if @event_metric.nil? || @pipeline_metric.nil?
|
108
124
|
@event_metric.increment(:in, batch.starting_size)
|
109
125
|
@pipeline_metric.increment(:in, batch.starting_size)
|
110
126
|
end
|
data/lib/logstash/version.rb
CHANGED
data/lib/logstash/webserver.rb
CHANGED
@@ -1,73 +1,84 @@
|
|
1
1
|
# encoding: utf-8
|
2
|
+
require "logstash/api/rack_app"
|
2
3
|
require "puma"
|
3
4
|
require "puma/server"
|
4
|
-
require "
|
5
|
+
require "concurrent"
|
5
6
|
|
6
|
-
module LogStash
|
7
|
+
module LogStash
|
7
8
|
class WebServer
|
8
9
|
extend Forwardable
|
9
10
|
|
10
|
-
attr_reader :logger, :status, :config, :options, :
|
11
|
+
attr_reader :logger, :status, :config, :options, :runner, :binder, :events, :http_host, :http_ports, :http_environment, :agent
|
11
12
|
|
12
13
|
def_delegator :@runner, :stats
|
13
14
|
|
14
15
|
DEFAULT_HOST = "127.0.0.1".freeze
|
15
|
-
|
16
|
+
DEFAULT_PORTS = (9600..9700).freeze
|
16
17
|
DEFAULT_ENVIRONMENT = 'production'.freeze
|
17
18
|
|
18
19
|
def initialize(logger, agent, options={})
|
19
20
|
@logger = logger
|
20
21
|
@agent = agent
|
21
22
|
@http_host = options[:http_host] || DEFAULT_HOST
|
22
|
-
@
|
23
|
+
@http_ports = options[:http_ports] || DEFAULT_PORTS
|
23
24
|
@http_environment = options[:http_environment] || DEFAULT_ENVIRONMENT
|
24
25
|
@options = {}
|
25
|
-
@
|
26
|
-
|
27
|
-
:debug => logger.debug?,
|
28
|
-
# Prevent puma from queueing request when not able to properly handling them,
|
29
|
-
# fixed https://github.com/elastic/logstash/issues/4674. See
|
30
|
-
# https://github.com/puma/puma/pull/640 for mode internal details in PUMA.
|
31
|
-
:queue_requests => false
|
32
|
-
})
|
33
|
-
@status = nil
|
26
|
+
@status = nil
|
27
|
+
@running = Concurrent::AtomicBoolean.new(false)
|
34
28
|
end
|
35
29
|
|
36
30
|
def run
|
37
|
-
|
31
|
+
logger.debug("Starting puma")
|
38
32
|
|
39
33
|
stop # Just in case
|
40
34
|
|
41
|
-
|
42
|
-
@server = ::Puma::Server.new(app)
|
43
|
-
@server.add_tcp_listener(http_host, http_port)
|
35
|
+
running!
|
44
36
|
|
45
|
-
|
46
|
-
|
47
|
-
|
48
|
-
|
37
|
+
http_ports.each_with_index do |port, idx|
|
38
|
+
begin
|
39
|
+
if running?
|
40
|
+
@port = port
|
41
|
+
logger.debug("Trying to start WebServer", :port => @port)
|
42
|
+
start_webserver(@port)
|
43
|
+
else
|
44
|
+
break # we are closing down the server so just get out of the loop
|
45
|
+
end
|
46
|
+
rescue Errno::EADDRINUSE
|
47
|
+
if http_ports.count == 1
|
48
|
+
raise Errno::EADDRINUSE.new(I18n.t("logstash.web_api.cant_bind_to_port", :port => http_ports.first))
|
49
|
+
elsif idx == http_ports.count-1
|
50
|
+
raise Errno::EADDRINUSE.new(I18n.t("logstash.web_api.cant_bind_to_port_in_range", :http_ports => http_ports))
|
51
|
+
end
|
52
|
+
end
|
53
|
+
end
|
49
54
|
end
|
50
55
|
|
51
|
-
def
|
52
|
-
|
56
|
+
def running!
|
57
|
+
@running.make_true
|
53
58
|
end
|
54
59
|
|
55
|
-
def
|
56
|
-
|
60
|
+
def running?
|
61
|
+
@running.value
|
57
62
|
end
|
58
63
|
|
59
64
|
def address
|
60
|
-
"#{http_host}:#{
|
65
|
+
"#{http_host}:#{@port}"
|
61
66
|
end
|
62
|
-
|
63
|
-
# Empty method, this method is required because of the puma usage we make through
|
64
|
-
# the Single interface, https://github.com/puma/puma/blob/master/lib/puma/single.rb#L82
|
65
|
-
# for more details. This can always be implemented when we want to keep track of this
|
66
|
-
# bit of data.
|
67
|
-
def write_state; end
|
68
67
|
|
69
68
|
def stop(options={})
|
69
|
+
@running.make_false
|
70
70
|
@server.stop(true) if @server
|
71
71
|
end
|
72
|
+
|
73
|
+
def start_webserver(port)
|
74
|
+
app = LogStash::Api::RackApp.app(logger, agent, http_environment)
|
75
|
+
|
76
|
+
@server = ::Puma::Server.new(app)
|
77
|
+
@server.add_tcp_listener(http_host, port)
|
78
|
+
|
79
|
+
logger.info("Succesfully started Logstash API", :port => @port)
|
80
|
+
|
81
|
+
@server.run.join
|
82
|
+
end
|
72
83
|
end
|
73
84
|
end
|
data/locales/en.yml
CHANGED
@@ -73,12 +73,16 @@ en:
|
|
73
73
|
non_reloadable_config_register: |-
|
74
74
|
Logstash is not able to start since configuration auto reloading was enabled but the configuration contains plugins that don't support it. Quitting...
|
75
75
|
web_api:
|
76
|
+
cant_bind_to_port: |-
|
77
|
+
Logstash tried to bind to port %{port}, but the port is already in use. You can specify a new port by launching logtash with the --http-port option."
|
78
|
+
cant_bind_to_port_in_range: |-
|
79
|
+
Logstash tried to bind to port range %{http_ports}, but all the ports are already in use. You can specify a new port by launching logtash with the --http-port option."
|
76
80
|
hot_threads:
|
77
81
|
title: |-
|
78
82
|
::: {%{hostname}}
|
79
83
|
Hot threads at %{time}, busiestThreads=%{top_count}:
|
80
84
|
thread_title: |-
|
81
|
-
|
85
|
+
%{percent_of_cpu_time} % of cpu usage, state: %{thread_state}, thread name: '%{thread_name}'
|
82
86
|
runner:
|
83
87
|
short-help: |-
|
84
88
|
usage:
|
data/logstash-core.gemspec
CHANGED
@@ -17,7 +17,7 @@ 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-java", "5.0.0.
|
20
|
+
gem.add_runtime_dependency "logstash-core-event-java", "5.0.0.alpha6.snapshot1"
|
21
21
|
|
22
22
|
gem.add_runtime_dependency "cabin", "~> 0.8.0" #(Apache 2.0 license)
|
23
23
|
gem.add_runtime_dependency "pry", "~> 0.10.1" #(Ruby license)
|
@@ -46,7 +46,7 @@ Gem::Specification.new do |gem|
|
|
46
46
|
|
47
47
|
if RUBY_PLATFORM == 'java'
|
48
48
|
gem.platform = RUBY_PLATFORM
|
49
|
-
gem.add_runtime_dependency "jrjackson", "~> 0.
|
49
|
+
gem.add_runtime_dependency "jrjackson", "~> 0.4.0" #(Apache 2.0 license)
|
50
50
|
else
|
51
51
|
gem.add_runtime_dependency "oj" #(MIT-style license)
|
52
52
|
end
|
@@ -39,30 +39,81 @@ describe LogStash::Api::Modules::Node do
|
|
39
39
|
end
|
40
40
|
|
41
41
|
context "when asking for human output" do
|
42
|
+
[
|
43
|
+
"/hot_threads?human",
|
44
|
+
"/hot_threads?human=true",
|
45
|
+
"/hot_threads?human=1",
|
46
|
+
"/hot_threads?human=t",
|
47
|
+
].each do |path|
|
48
|
+
|
49
|
+
before(:all) do
|
50
|
+
do_request { get path }
|
51
|
+
end
|
52
|
+
|
53
|
+
let(:payload) { last_response.body }
|
54
|
+
|
55
|
+
it "should return a text/plain content type" do
|
56
|
+
expect(last_response.content_type).to eq("text/plain;charset=utf-8")
|
57
|
+
end
|
58
|
+
|
59
|
+
it "should return a plain text payload" do
|
60
|
+
expect{ JSON.parse(payload) }.to raise_error
|
61
|
+
end
|
62
|
+
end
|
63
|
+
end
|
42
64
|
|
65
|
+
context "When asking for human output and threads count" do
|
43
66
|
before(:all) do
|
44
|
-
|
67
|
+
# Make sure we have enough threads for this to work.
|
68
|
+
@threads = []
|
69
|
+
5.times { @threads << Thread.new { loop {} } }
|
70
|
+
|
71
|
+
do_request { get "/hot_threads?human=t&threads=2"}
|
72
|
+
end
|
73
|
+
|
74
|
+
after(:all) do
|
75
|
+
@threads.each { |t| t.kill } rescue nil
|
45
76
|
end
|
46
77
|
|
47
78
|
let(:payload) { last_response.body }
|
48
79
|
|
49
|
-
it "should return
|
50
|
-
expect(
|
80
|
+
it "should return information for <= # requested threads" do
|
81
|
+
expect(payload.scan(/thread name/).size).to eq(2)
|
51
82
|
end
|
83
|
+
end
|
52
84
|
|
53
|
-
|
54
|
-
|
85
|
+
context "when not asking for human output" do
|
86
|
+
[
|
87
|
+
"/hot_threads?human=false",
|
88
|
+
"/hot_threads?human=0",
|
89
|
+
"/hot_threads?human=f",
|
90
|
+
].each do |path|
|
91
|
+
before(:all) do
|
92
|
+
do_request { get path }
|
93
|
+
end
|
94
|
+
|
95
|
+
it "should return a json payload content type" do
|
96
|
+
expect(last_response.content_type).to eq("application/json")
|
97
|
+
end
|
98
|
+
|
99
|
+
let(:payload) { last_response.body }
|
100
|
+
|
101
|
+
it "should return a json payload" do
|
102
|
+
expect{ JSON.parse(payload) }.not_to raise_error
|
103
|
+
end
|
55
104
|
end
|
56
105
|
end
|
57
106
|
|
58
107
|
describe "Generic JSON testing" do
|
59
108
|
extend ResourceDSLMethods
|
60
|
-
|
109
|
+
|
61
110
|
root_structure = {
|
62
111
|
"pipeline" => {
|
63
112
|
"workers" => Numeric,
|
64
113
|
"batch_size" => Numeric,
|
65
|
-
"batch_delay" => Numeric
|
114
|
+
"batch_delay" => Numeric,
|
115
|
+
"config_reload_automatic" => Boolean,
|
116
|
+
"config_reload_interval" => Numeric
|
66
117
|
},
|
67
118
|
"os" => {
|
68
119
|
"name" => String,
|
@@ -82,7 +133,8 @@ describe LogStash::Api::Modules::Node do
|
|
82
133
|
"heap_max_in_bytes" => Numeric,
|
83
134
|
"non_heap_init_in_bytes" => Numeric,
|
84
135
|
"non_heap_max_in_bytes" => Numeric
|
85
|
-
|
136
|
+
},
|
137
|
+
"gc_collectors" => Array
|
86
138
|
},
|
87
139
|
"hot_threads"=> {
|
88
140
|
"time" => String,
|
@@ -90,8 +142,8 @@ describe LogStash::Api::Modules::Node do
|
|
90
142
|
"threads" => Array
|
91
143
|
}
|
92
144
|
}
|
93
|
-
|
145
|
+
|
94
146
|
test_api_and_resources(root_structure, :exclude_from_root => ["hot_threads"])
|
95
|
-
end
|
147
|
+
end
|
96
148
|
end
|
97
149
|
end
|