fluentd 0.14.2 → 0.14.5
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/ChangeLog +77 -1131
- data/Rakefile +15 -0
- data/appveyor.yml +2 -2
- data/example/in_forward.conf +3 -0
- data/example/in_forward_client.conf +37 -0
- data/example/in_forward_shared_key.conf +15 -0
- data/example/in_forward_users.conf +24 -0
- data/example/multi_filters.conf +61 -0
- data/example/out_forward.conf +13 -13
- data/example/out_forward_client.conf +109 -0
- data/example/out_forward_shared_key.conf +36 -0
- data/example/out_forward_users.conf +65 -0
- data/example/{out_buffered_null.conf → out_null.conf} +10 -6
- data/example/secondary_file.conf +41 -0
- data/fluentd.gemspec +19 -16
- data/lib/fluent/agent.rb +5 -0
- data/lib/fluent/command/debug.rb +4 -4
- data/lib/fluent/compat/handle_tag_and_time_mixin.rb +2 -0
- data/lib/fluent/compat/output.rb +5 -2
- data/lib/fluent/config/configure_proxy.rb +26 -5
- data/lib/fluent/config/error.rb +3 -0
- data/lib/fluent/config/section.rb +15 -0
- data/lib/fluent/event_router.rb +77 -4
- data/lib/fluent/plugin/base.rb +12 -3
- data/lib/fluent/plugin/buffer.rb +5 -1
- data/lib/fluent/plugin/filter.rb +48 -6
- data/lib/fluent/plugin/filter_record_transformer.rb +3 -1
- data/lib/fluent/plugin/filter_stdout.rb +0 -4
- data/lib/fluent/plugin/in_debug_agent.rb +5 -5
- data/lib/fluent/plugin/in_dummy.rb +9 -1
- data/lib/fluent/plugin/in_forward.rb +330 -62
- data/lib/fluent/plugin/in_monitor_agent.rb +31 -77
- data/lib/fluent/plugin/in_tail.rb +76 -92
- data/lib/fluent/plugin/multi_output.rb +4 -0
- data/lib/fluent/plugin/out_forward.rb +328 -222
- data/lib/fluent/plugin/out_null.rb +37 -0
- data/lib/fluent/plugin/out_secondary_file.rb +128 -0
- data/lib/fluent/plugin/out_stdout.rb +38 -2
- data/lib/fluent/plugin/output.rb +29 -6
- data/lib/fluent/plugin/storage_local.rb +16 -0
- data/lib/fluent/plugin_helper/timer.rb +6 -1
- data/lib/fluent/root_agent.rb +4 -1
- data/lib/fluent/supervisor.rb +62 -9
- data/lib/fluent/test/base.rb +3 -0
- data/lib/fluent/test/driver/base.rb +5 -0
- data/lib/fluent/test/formatter_test.rb +2 -0
- data/lib/fluent/test/parser_test.rb +2 -0
- data/lib/fluent/test/startup_shutdown.rb +33 -0
- data/lib/fluent/version.rb +1 -1
- data/lib/fluent/winsvc.rb +1 -2
- data/test/compat/test_calls_super.rb +2 -0
- data/test/config/test_configurable.rb +88 -0
- data/test/config/test_types.rb +7 -3
- data/test/plugin/test_filter.rb +121 -23
- data/test/plugin/test_filter_record_transformer.rb +22 -6
- data/test/plugin/test_in_debug_agent.rb +2 -2
- data/test/plugin/test_in_forward.rb +920 -407
- data/test/plugin/test_in_monitor_agent.rb +333 -0
- data/test/plugin/test_in_tail.rb +686 -595
- data/test/plugin/test_out_forward.rb +150 -207
- data/test/plugin/test_out_null.rb +85 -9
- data/test/plugin/test_out_secondary_file.rb +432 -0
- data/test/plugin/test_out_stdout.rb +143 -45
- data/test/plugin/test_output.rb +53 -0
- data/test/plugin/test_output_as_buffered.rb +13 -0
- data/test/plugin/test_output_as_buffered_overflow.rb +3 -0
- data/test/plugin/test_output_as_buffered_retries.rb +11 -0
- data/test/plugin/test_output_as_buffered_secondary.rb +12 -0
- data/test/plugin/test_output_as_standard.rb +12 -0
- data/test/plugin_helper/test_compat_parameters.rb +2 -0
- data/test/plugin_helper/test_timer.rb +31 -0
- data/test/test_event_router.rb +87 -0
- data/test/test_filter.rb +48 -6
- data/test/test_log.rb +5 -2
- data/test/test_output.rb +41 -1
- data/test/test_plugin_classes.rb +17 -9
- data/test/test_root_agent.rb +188 -0
- metadata +62 -68
- data/lib/fluent/plugin/out_buffered_null.rb +0 -59
- data/lib/fluent/plugin/out_buffered_stdout.rb +0 -70
- data/test/plugin/test_out_buffered_null.rb +0 -79
- data/test/plugin/test_out_buffered_stdout.rb +0 -122
|
@@ -1,46 +1,73 @@
|
|
|
1
1
|
require_relative '../helper'
|
|
2
2
|
|
|
3
3
|
require 'fluent/test'
|
|
4
|
+
require 'fluent/test/startup_shutdown'
|
|
4
5
|
require 'base64'
|
|
5
6
|
|
|
6
7
|
require 'fluent/env'
|
|
7
8
|
require 'fluent/plugin/in_forward'
|
|
8
9
|
|
|
9
10
|
class ForwardInputTest < Test::Unit::TestCase
|
|
10
|
-
class << self
|
|
11
|
-
def startup
|
|
12
|
-
socket_manager_path = ServerEngine::SocketManager::Server.generate_path
|
|
13
|
-
@server = ServerEngine::SocketManager::Server.open(socket_manager_path)
|
|
14
|
-
ENV['SERVERENGINE_SOCKETMANAGER_PATH'] = socket_manager_path.to_s
|
|
15
|
-
end
|
|
16
|
-
|
|
17
|
-
def shutdown
|
|
18
|
-
@server.close
|
|
19
|
-
end
|
|
20
|
-
end
|
|
21
|
-
|
|
22
11
|
def setup
|
|
23
12
|
Fluent::Test.setup
|
|
24
13
|
@responses = [] # for testing responses after sending data
|
|
25
14
|
end
|
|
26
15
|
|
|
27
16
|
PORT = unused_port
|
|
17
|
+
|
|
18
|
+
SHARED_KEY = 'foobar1'
|
|
19
|
+
USER_NAME = 'tagomoris'
|
|
20
|
+
USER_PASSWORD = 'fluentd'
|
|
21
|
+
|
|
28
22
|
CONFIG = %[
|
|
29
23
|
port #{PORT}
|
|
30
24
|
bind 127.0.0.1
|
|
31
25
|
]
|
|
26
|
+
PEERADDR = ['?', '0000', '127.0.0.1', '127.0.0.1']
|
|
27
|
+
CONFIG_AUTH = %[
|
|
28
|
+
port #{PORT}
|
|
29
|
+
bind 127.0.0.1
|
|
30
|
+
<security>
|
|
31
|
+
self_hostname localhost
|
|
32
|
+
shared_key foobar1
|
|
33
|
+
user_auth true
|
|
34
|
+
<user>
|
|
35
|
+
username #{USER_NAME}
|
|
36
|
+
password #{USER_PASSWORD}
|
|
37
|
+
</user>
|
|
38
|
+
<client>
|
|
39
|
+
network 127.0.0.0/8
|
|
40
|
+
shared_key #{SHARED_KEY}
|
|
41
|
+
users ["#{USER_NAME}"]
|
|
42
|
+
</client>
|
|
43
|
+
</security>
|
|
44
|
+
]
|
|
32
45
|
|
|
33
46
|
def create_driver(conf=CONFIG)
|
|
34
47
|
Fluent::Test::InputTestDriver.new(Fluent::ForwardInput).configure(conf)
|
|
35
48
|
end
|
|
36
49
|
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
50
|
+
class Configure < self
|
|
51
|
+
def test_simple
|
|
52
|
+
d = create_driver
|
|
53
|
+
assert_equal PORT, d.instance.port
|
|
54
|
+
assert_equal '127.0.0.1', d.instance.bind
|
|
55
|
+
assert_equal 0, d.instance.linger_timeout
|
|
56
|
+
assert_equal 0.5, d.instance.blocking_timeout
|
|
57
|
+
assert !d.instance.backlog
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
def test_auth
|
|
61
|
+
d = create_driver(CONFIG_AUTH)
|
|
62
|
+
assert_equal PORT, d.instance.port
|
|
63
|
+
assert_equal '127.0.0.1', d.instance.bind
|
|
64
|
+
assert_equal 0, d.instance.linger_timeout
|
|
65
|
+
assert !d.instance.backlog
|
|
66
|
+
|
|
67
|
+
assert d.instance.security
|
|
68
|
+
assert_equal 1, d.instance.security.users.size
|
|
69
|
+
assert_equal 1, d.instance.security.clients.size
|
|
70
|
+
end
|
|
44
71
|
end
|
|
45
72
|
|
|
46
73
|
# TODO: Will add Loop::run arity check with stub/mock library
|
|
@@ -49,538 +76,1024 @@ class ForwardInputTest < Test::Unit::TestCase
|
|
|
49
76
|
TCPSocket.new('127.0.0.1', PORT)
|
|
50
77
|
end
|
|
51
78
|
|
|
52
|
-
|
|
53
|
-
|
|
79
|
+
class Message < self
|
|
80
|
+
extend Fluent::Test::StartupShutdown
|
|
54
81
|
|
|
55
|
-
|
|
56
|
-
|
|
82
|
+
def test_time
|
|
83
|
+
d = create_driver
|
|
57
84
|
|
|
58
|
-
|
|
59
|
-
|
|
85
|
+
time = Fluent::EventTime.parse("2011-01-02 13:14:15 UTC")
|
|
86
|
+
Fluent::Engine.now = time
|
|
60
87
|
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
end
|
|
66
|
-
end
|
|
67
|
-
|
|
68
|
-
def test_message
|
|
69
|
-
d = create_driver
|
|
88
|
+
records = [
|
|
89
|
+
["tag1", time, {"a"=>1}],
|
|
90
|
+
["tag2", time, {"a"=>2}],
|
|
91
|
+
]
|
|
70
92
|
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
}
|
|
93
|
+
d.expected_emits_length = records.length
|
|
94
|
+
d.run_timeout = 2
|
|
95
|
+
d.run do
|
|
96
|
+
records.each {|tag, _time, record|
|
|
97
|
+
send_data packer.write([tag, 0, record]).to_s
|
|
98
|
+
}
|
|
99
|
+
end
|
|
100
|
+
assert_equal(records, d.emits.sort_by {|a| a[0] })
|
|
80
101
|
end
|
|
81
|
-
end
|
|
82
102
|
|
|
83
|
-
|
|
84
|
-
|
|
103
|
+
def test_plain
|
|
104
|
+
d = create_driver
|
|
85
105
|
|
|
86
|
-
|
|
106
|
+
time = Fluent::EventTime.parse("2011-01-02 13:14:15 UTC")
|
|
87
107
|
|
|
88
|
-
|
|
89
|
-
|
|
108
|
+
records = [
|
|
109
|
+
["tag1", time, {"a"=>1}],
|
|
110
|
+
["tag2", time, {"a"=>2}],
|
|
111
|
+
]
|
|
90
112
|
|
|
91
|
-
|
|
92
|
-
d.
|
|
93
|
-
|
|
94
|
-
|
|
113
|
+
d.expected_emits_length = records.length
|
|
114
|
+
d.run_timeout = 2
|
|
115
|
+
d.run do
|
|
116
|
+
records.each {|tag, _time, record|
|
|
117
|
+
send_data packer.write([tag, _time, record]).to_s
|
|
118
|
+
}
|
|
119
|
+
end
|
|
120
|
+
assert_equal(records, d.emits)
|
|
95
121
|
end
|
|
96
|
-
end
|
|
97
122
|
|
|
98
|
-
|
|
99
|
-
|
|
123
|
+
def test_time_as_integer
|
|
124
|
+
d = create_driver
|
|
100
125
|
|
|
101
|
-
|
|
126
|
+
time = Time.parse("2011-01-02 13:14:15 UTC").to_i
|
|
102
127
|
|
|
103
|
-
|
|
104
|
-
|
|
128
|
+
records = [
|
|
129
|
+
["tag1", time, {"a"=>1}],
|
|
130
|
+
["tag2", time, {"a"=>2}],
|
|
131
|
+
]
|
|
105
132
|
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
133
|
+
d.expected_emits_length = records.length
|
|
134
|
+
d.run_timeout = 2
|
|
135
|
+
d.run do
|
|
136
|
+
records.each {|tag, _time, record|
|
|
137
|
+
send_data packer.write([tag, _time, record]).to_s
|
|
138
|
+
}
|
|
139
|
+
end
|
|
110
140
|
|
|
111
|
-
|
|
112
|
-
# Without ack, logs are sometimes not saved to logs during test.
|
|
113
|
-
send_data Fluent::Engine.msgpack_factory.packer.write([tag, _time, record]).to_s, true
|
|
114
|
-
}
|
|
141
|
+
assert_equal(records, d.emits)
|
|
115
142
|
end
|
|
116
143
|
|
|
117
|
-
|
|
118
|
-
|
|
144
|
+
def test_skip_invalid_event
|
|
145
|
+
d = create_driver(CONFIG + "skip_invalid_event true")
|
|
119
146
|
|
|
120
|
-
|
|
121
|
-
d = create_driver
|
|
147
|
+
time = Fluent::EventTime.parse("2011-01-02 13:14:15 UTC")
|
|
122
148
|
|
|
123
|
-
|
|
149
|
+
records = [
|
|
150
|
+
["tag1", time, {"a" => 1}],
|
|
151
|
+
["tag2", time, {"a" => 2}],
|
|
152
|
+
]
|
|
124
153
|
|
|
125
|
-
|
|
126
|
-
|
|
154
|
+
d.run do
|
|
155
|
+
entries = records.map { |tag, _time, record| [tag, _time, record] }
|
|
156
|
+
# These entries are skipped
|
|
157
|
+
entries << ['tag1', true, {'a' => 3}] << ['tag2', time, 'invalid record']
|
|
127
158
|
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
send_data Fluent::Engine.msgpack_factory.packer.write(["tag1", entries]).to_s
|
|
134
|
-
end
|
|
135
|
-
end
|
|
136
|
-
|
|
137
|
-
def test_forward_with_time_as_integer
|
|
138
|
-
d = create_driver
|
|
139
|
-
|
|
140
|
-
time = Time.parse("2011-01-02 13:14:15 UTC").to_i
|
|
141
|
-
|
|
142
|
-
d.expect_emit "tag1", time, {"a"=>1}
|
|
143
|
-
d.expect_emit "tag1", time, {"a"=>2}
|
|
159
|
+
entries.each {|tag, _time, record|
|
|
160
|
+
# Without ack, logs are sometimes not saved to logs during test.
|
|
161
|
+
send_data packer.write([tag, _time, record]).to_s, try_to_receive_response: true
|
|
162
|
+
}
|
|
163
|
+
end
|
|
144
164
|
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
entries << [_time, record]
|
|
149
|
-
}
|
|
150
|
-
send_data Fluent::Engine.msgpack_factory.packer.write(["tag1", entries]).to_s
|
|
165
|
+
assert_equal 2, d.instance.log.logs.count { |line| line =~ /got invalid event and drop it/ }
|
|
166
|
+
assert_equal records[0], d.emits[0]
|
|
167
|
+
assert_equal records[1], d.emits[1]
|
|
151
168
|
end
|
|
152
|
-
end
|
|
153
169
|
|
|
154
|
-
|
|
155
|
-
|
|
170
|
+
def test_json
|
|
171
|
+
d = create_driver
|
|
156
172
|
|
|
157
|
-
|
|
173
|
+
time = Time.parse("2011-01-02 13:14:15 UTC").to_i
|
|
158
174
|
|
|
159
|
-
|
|
160
|
-
|
|
175
|
+
records = [
|
|
176
|
+
["tag1", time, {"a"=>1}],
|
|
177
|
+
["tag2", time, {"a"=>2}],
|
|
178
|
+
]
|
|
161
179
|
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
180
|
+
d.expected_emits_length = records.length
|
|
181
|
+
d.run_timeout = 2
|
|
182
|
+
d.run do
|
|
183
|
+
records.each {|tag, _time, record|
|
|
184
|
+
send_data [tag, _time, record].to_json
|
|
185
|
+
}
|
|
186
|
+
end
|
|
166
187
|
|
|
167
|
-
|
|
188
|
+
assert_equal(records, d.emits.sort_by {|a| a[1] })
|
|
168
189
|
end
|
|
169
190
|
|
|
170
|
-
|
|
171
|
-
|
|
191
|
+
def test_json_with_newline
|
|
192
|
+
d = create_driver
|
|
172
193
|
|
|
173
|
-
|
|
174
|
-
d = create_driver
|
|
194
|
+
time = Time.parse("2011-01-02 13:14:15 UTC").to_i
|
|
175
195
|
|
|
176
|
-
|
|
196
|
+
records = [
|
|
197
|
+
["tag1", time, {"a"=>1}],
|
|
198
|
+
["tag2", time, {"a"=>2}],
|
|
199
|
+
]
|
|
177
200
|
|
|
178
|
-
|
|
179
|
-
|
|
201
|
+
d.expected_emits_length = records.length
|
|
202
|
+
d.run_timeout = 2
|
|
203
|
+
d.run do
|
|
204
|
+
records.each {|tag, _time, record|
|
|
205
|
+
send_data [tag, _time, record].to_json + "\n"
|
|
206
|
+
}
|
|
207
|
+
end
|
|
180
208
|
|
|
181
|
-
|
|
182
|
-
entries = ''
|
|
183
|
-
d.expected_emits.each {|_tag, _time, record|
|
|
184
|
-
Fluent::Engine.msgpack_factory.packer(entries).write([_time, record]).flush
|
|
185
|
-
}
|
|
186
|
-
send_data Fluent::Engine.msgpack_factory.packer.write(["tag1", entries]).to_s
|
|
209
|
+
assert_equal(records, d.emits.sort_by {|a| a[1] })
|
|
187
210
|
end
|
|
188
211
|
end
|
|
189
212
|
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
213
|
+
class Forward < self
|
|
214
|
+
extend Fluent::Test::StartupShutdown
|
|
215
|
+
|
|
216
|
+
data(tcp: {
|
|
217
|
+
config: CONFIG,
|
|
218
|
+
options: {
|
|
219
|
+
auth: false
|
|
220
|
+
}
|
|
221
|
+
},
|
|
222
|
+
auth: {
|
|
223
|
+
config: CONFIG_AUTH,
|
|
224
|
+
options: {
|
|
225
|
+
auth: true
|
|
226
|
+
}
|
|
227
|
+
})
|
|
228
|
+
def test_plain(data)
|
|
229
|
+
config = data[:config]
|
|
230
|
+
options = data[:options]
|
|
231
|
+
d = create_driver(config)
|
|
232
|
+
|
|
233
|
+
time = Fluent::EventTime.parse("2011-01-02 13:14:15 UTC")
|
|
234
|
+
|
|
235
|
+
records = [
|
|
236
|
+
["tag1", time, {"a"=>1}],
|
|
237
|
+
["tag1", time, {"a"=>2}]
|
|
238
|
+
]
|
|
239
|
+
|
|
240
|
+
d.expected_emits_length = records.length
|
|
241
|
+
d.run_timeout = 2
|
|
242
|
+
d.run do
|
|
243
|
+
sleep 0.1 until d.instance.instance_eval{ @thread } && d.instance.instance_eval{ @thread }.status
|
|
244
|
+
|
|
245
|
+
entries = []
|
|
246
|
+
records.each {|tag, _time, record|
|
|
247
|
+
entries << [_time, record]
|
|
248
|
+
}
|
|
249
|
+
send_data packer.write(["tag1", entries]).to_s, **options
|
|
250
|
+
end
|
|
251
|
+
assert_equal(records, d.emits)
|
|
197
252
|
|
|
198
|
-
|
|
199
|
-
entries = ''
|
|
200
|
-
d.expected_emits.each {|_tag, _time, record|
|
|
201
|
-
Fluent::Engine.msgpack_factory.packer(entries).write([_time, record]).flush
|
|
202
|
-
}
|
|
203
|
-
send_data Fluent::Engine.msgpack_factory.packer.write(["tag1", entries]).to_s
|
|
253
|
+
sleep 0.1 while d.instance.instance_eval{ @thread }.status # to confirm that plugin stopped completely
|
|
204
254
|
end
|
|
205
|
-
end
|
|
206
|
-
|
|
207
|
-
def test_packed_forward_with_skip_invalid_event
|
|
208
|
-
d = create_driver(CONFIG + "skip_invalid_event true")
|
|
209
|
-
|
|
210
|
-
time = Fluent::EventTime.parse("2011-01-02 13:14:15 UTC")
|
|
211
255
|
|
|
212
|
-
|
|
213
|
-
|
|
256
|
+
data(tcp: {
|
|
257
|
+
config: CONFIG,
|
|
258
|
+
options: {
|
|
259
|
+
auth: false
|
|
260
|
+
}
|
|
261
|
+
},
|
|
262
|
+
auth: {
|
|
263
|
+
config: CONFIG_AUTH,
|
|
264
|
+
options: {
|
|
265
|
+
auth: true
|
|
266
|
+
}
|
|
267
|
+
})
|
|
268
|
+
def test_time_as_integer(data)
|
|
269
|
+
config = data[:config]
|
|
270
|
+
options = data[:options]
|
|
271
|
+
d = create_driver(config)
|
|
272
|
+
|
|
273
|
+
time = Time.parse("2011-01-02 13:14:15 UTC").to_i
|
|
274
|
+
|
|
275
|
+
records = [
|
|
276
|
+
["tag1", time, {"a"=>1}],
|
|
277
|
+
["tag1", time, {"a"=>2}]
|
|
278
|
+
]
|
|
279
|
+
|
|
280
|
+
d.expected_emits_length = records.length
|
|
281
|
+
d.run_timeout = 2
|
|
282
|
+
d.run do
|
|
283
|
+
sleep 0.1 until d.instance.instance_eval{ @thread } && d.instance.instance_eval{ @thread }.status
|
|
284
|
+
|
|
285
|
+
entries = []
|
|
286
|
+
records.each {|tag, _time, record|
|
|
287
|
+
entries << [_time, record]
|
|
288
|
+
}
|
|
289
|
+
send_data packer.write(["tag1", entries]).to_s, **options
|
|
290
|
+
end
|
|
214
291
|
|
|
215
|
-
|
|
216
|
-
entries = d.expected_emits.map {|_tag , _time, record| [_time, record] }
|
|
217
|
-
# These entries are skipped
|
|
218
|
-
entries << ['invalid time', {'a' => 3}] << [time, 'invalid record']
|
|
292
|
+
assert_equal(records, d.emits)
|
|
219
293
|
|
|
220
|
-
|
|
221
|
-
entries.each {|_time, record|
|
|
222
|
-
Fluent::Engine.msgpack_factory.packer(packed_entries).write([_time, record]).flush
|
|
223
|
-
}
|
|
224
|
-
send_data Fluent::Engine.msgpack_factory.packer.write(["tag1", packed_entries]).to_s
|
|
294
|
+
sleep 0.1 while d.instance.instance_eval{ @thread }.status # to confirm that plugin stopped completely
|
|
225
295
|
end
|
|
226
296
|
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
297
|
+
data(tcp: {
|
|
298
|
+
config: CONFIG,
|
|
299
|
+
options: {
|
|
300
|
+
auth: false
|
|
301
|
+
}
|
|
302
|
+
},
|
|
303
|
+
auth: {
|
|
304
|
+
config: CONFIG_AUTH,
|
|
305
|
+
options: {
|
|
306
|
+
auth: true
|
|
307
|
+
}
|
|
308
|
+
})
|
|
309
|
+
def test_skip_invalid_event(data)
|
|
310
|
+
config = data[:config]
|
|
311
|
+
options = data[:options]
|
|
312
|
+
d = create_driver(config + "skip_invalid_event true")
|
|
313
|
+
|
|
314
|
+
time = Fluent::EventTime.parse("2011-01-02 13:14:15 UTC")
|
|
315
|
+
|
|
316
|
+
records = [
|
|
317
|
+
["tag1", time, {"a" => 1}],
|
|
318
|
+
["tag1", time, {"a" => 2}],
|
|
319
|
+
]
|
|
320
|
+
|
|
321
|
+
d.expected_emits_length = records.length
|
|
322
|
+
d.run_timeout = 2
|
|
323
|
+
d.run do
|
|
324
|
+
sleep 0.1 until d.instance.instance_eval{ @thread } && d.instance.instance_eval{ @thread }.status
|
|
325
|
+
|
|
326
|
+
entries = records.map { |tag, _time, record| [_time, record] }
|
|
327
|
+
# These entries are skipped
|
|
328
|
+
entries << ['invalid time', {'a' => 3}] << [time, 'invalid record']
|
|
329
|
+
|
|
330
|
+
send_data packer.write(["tag1", entries]).to_s, **options
|
|
331
|
+
end
|
|
234
332
|
|
|
235
|
-
|
|
236
|
-
d.expect_emit "tag2", time, {"a"=>2}
|
|
333
|
+
assert_equal 2, d.instance.log.logs.count { |line| line =~ /skip invalid event/ }
|
|
237
334
|
|
|
238
|
-
|
|
239
|
-
d.expected_emits.each {|tag, _time, record|
|
|
240
|
-
send_data [tag, _time, record].to_json
|
|
241
|
-
}
|
|
335
|
+
sleep 0.1 while d.instance.instance_eval{ @thread }.status # to confirm that plugin stopped completely
|
|
242
336
|
end
|
|
243
337
|
end
|
|
244
338
|
|
|
245
|
-
|
|
246
|
-
|
|
339
|
+
class PackedForward < self
|
|
340
|
+
extend Fluent::Test::StartupShutdown
|
|
341
|
+
|
|
342
|
+
data(tcp: {
|
|
343
|
+
config: CONFIG,
|
|
344
|
+
options: {
|
|
345
|
+
auth: false
|
|
346
|
+
}
|
|
347
|
+
},
|
|
348
|
+
auth: {
|
|
349
|
+
config: CONFIG_AUTH,
|
|
350
|
+
options: {
|
|
351
|
+
auth: true
|
|
352
|
+
}
|
|
353
|
+
})
|
|
354
|
+
def test_plain(data)
|
|
355
|
+
config = data[:config]
|
|
356
|
+
options = data[:options]
|
|
357
|
+
d = create_driver(config)
|
|
358
|
+
|
|
359
|
+
time = Fluent::EventTime.parse("2011-01-02 13:14:15 UTC")
|
|
360
|
+
|
|
361
|
+
records = [
|
|
362
|
+
["tag1", time, {"a"=>1}],
|
|
363
|
+
["tag1", time, {"a"=>2}],
|
|
364
|
+
]
|
|
365
|
+
|
|
366
|
+
d.expected_emits_length = records.length
|
|
367
|
+
d.run_timeout = 2
|
|
368
|
+
d.run do
|
|
369
|
+
sleep 0.1 until d.instance.instance_eval{ @thread } && d.instance.instance_eval{ @thread }.status
|
|
370
|
+
|
|
371
|
+
entries = ''
|
|
372
|
+
records.each {|_tag, _time, record|
|
|
373
|
+
packer(entries).write([_time, record]).flush
|
|
374
|
+
}
|
|
375
|
+
send_data packer.write(["tag1", entries]).to_s, **options
|
|
376
|
+
end
|
|
377
|
+
assert_equal(records, d.emits)
|
|
247
378
|
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
["tag1", time, {"a"=>1}],
|
|
251
|
-
["tag1", time, {"a"=>2}]
|
|
252
|
-
]
|
|
379
|
+
sleep 0.1 while d.instance.instance_eval{ @thread }.status # to confirm that plugin stopped completely
|
|
380
|
+
end
|
|
253
381
|
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
382
|
+
data(tcp: {
|
|
383
|
+
config: CONFIG,
|
|
384
|
+
options: {
|
|
385
|
+
auth: false
|
|
386
|
+
}
|
|
387
|
+
},
|
|
388
|
+
auth: {
|
|
389
|
+
config: CONFIG_AUTH,
|
|
390
|
+
options: {
|
|
391
|
+
auth: true
|
|
392
|
+
}
|
|
393
|
+
})
|
|
394
|
+
def test_time_as_integer(data)
|
|
395
|
+
config = data[:config]
|
|
396
|
+
options = data[:options]
|
|
397
|
+
d = create_driver(config)
|
|
398
|
+
|
|
399
|
+
time = Time.parse("2011-01-02 13:14:15 UTC").to_i
|
|
400
|
+
|
|
401
|
+
records = [
|
|
402
|
+
["tag1", time, {"a"=>1}],
|
|
403
|
+
["tag1", time, {"a"=>2}],
|
|
404
|
+
]
|
|
405
|
+
|
|
406
|
+
d.expected_emits_length = records.length
|
|
407
|
+
d.run_timeout = 2
|
|
408
|
+
d.run do
|
|
409
|
+
sleep 0.1 until d.instance.instance_eval{ @thread } && d.instance.instance_eval{ @thread }.status
|
|
410
|
+
|
|
411
|
+
entries = ''
|
|
412
|
+
records.each {|tag, _time, record|
|
|
413
|
+
packer(entries).write([_time, record]).flush
|
|
414
|
+
}
|
|
415
|
+
send_data packer.write(["tag1", entries]).to_s, **options
|
|
416
|
+
end
|
|
417
|
+
assert_equal(records, d.emits)
|
|
258
418
|
|
|
259
|
-
|
|
419
|
+
sleep 0.1 while d.instance.instance_eval{ @thread }.status # to confirm that plugin stopped completely
|
|
420
|
+
end
|
|
260
421
|
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
422
|
+
data(tcp: {
|
|
423
|
+
config: CONFIG,
|
|
424
|
+
options: {
|
|
425
|
+
auth: false
|
|
426
|
+
}
|
|
427
|
+
},
|
|
428
|
+
auth: {
|
|
429
|
+
config: CONFIG_AUTH,
|
|
430
|
+
options: {
|
|
431
|
+
auth: true
|
|
432
|
+
}
|
|
433
|
+
})
|
|
434
|
+
def test_skip_invalid_event(data)
|
|
435
|
+
config = data[:config]
|
|
436
|
+
options = data[:options]
|
|
437
|
+
d = create_driver(config + "skip_invalid_event true")
|
|
438
|
+
|
|
439
|
+
time = Fluent::EventTime.parse("2011-01-02 13:14:15 UTC")
|
|
440
|
+
|
|
441
|
+
records = [
|
|
442
|
+
["tag1", time, {"a" => 1}],
|
|
443
|
+
["tag1", time, {"a" => 2}],
|
|
444
|
+
]
|
|
445
|
+
|
|
446
|
+
d.expected_emits_length = records.length
|
|
447
|
+
d.run_timeout = 2
|
|
448
|
+
d.run do
|
|
449
|
+
sleep 0.1 until d.instance.instance_eval{ @thread } && d.instance.instance_eval{ @thread }.status
|
|
450
|
+
|
|
451
|
+
entries = records.map { |tag, _time, record| [_time, record] }
|
|
452
|
+
# These entries are skipped
|
|
453
|
+
entries << ['invalid time', {'a' => 3}] << [time, 'invalid record']
|
|
454
|
+
|
|
455
|
+
packed_entries = ''
|
|
456
|
+
entries.each { |_time, record|
|
|
457
|
+
packer(packed_entries).write([_time, record]).flush
|
|
458
|
+
}
|
|
459
|
+
send_data packer.write(["tag1", packed_entries]).to_s, **options
|
|
265
460
|
end
|
|
461
|
+
|
|
462
|
+
assert_equal 2, d.instance.log.logs.count { |line| line =~ /skip invalid event/ }
|
|
463
|
+
|
|
464
|
+
sleep 0.1 while d.instance.instance_eval{ @thread }.status # to confirm that plugin stopped completely
|
|
266
465
|
end
|
|
267
466
|
end
|
|
268
467
|
|
|
269
|
-
|
|
270
|
-
|
|
468
|
+
class Warning < self
|
|
469
|
+
extend Fluent::Test::StartupShutdown
|
|
470
|
+
|
|
471
|
+
def test_send_large_chunk_warning
|
|
472
|
+
d = create_driver(CONFIG + %[
|
|
271
473
|
chunk_size_warn_limit 16M
|
|
272
474
|
chunk_size_limit 32M
|
|
273
475
|
])
|
|
274
476
|
|
|
275
|
-
|
|
477
|
+
time = Fluent::EventTime.parse("2014-04-25 13:14:15 UTC")
|
|
276
478
|
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
479
|
+
# generate over 16M chunk
|
|
480
|
+
str = "X" * 1024 * 1024
|
|
481
|
+
chunk = [ "test.tag", (0...16).map{|i| [time + i, {"data" => str}] } ].to_msgpack
|
|
482
|
+
assert chunk.size > (16 * 1024 * 1024)
|
|
483
|
+
assert chunk.size < (32 * 1024 * 1024)
|
|
282
484
|
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
d.instance.send(:on_message, obj, chunk.size, "host: 127.0.0.1, addr: 127.0.0.1, port: 0000")
|
|
286
|
-
end
|
|
287
|
-
end
|
|
485
|
+
d.run do
|
|
486
|
+
sleep 0.1 until d.instance.instance_eval{ @thread } && d.instance.instance_eval{ @thread }.status
|
|
288
487
|
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
assert_equal (0...16).to_a, emits.map{|_tag, t, _record| t - time }
|
|
488
|
+
Fluent::Engine.msgpack_factory.unpacker.feed_each(chunk) do |obj|
|
|
489
|
+
d.instance.send(:on_message, obj, chunk.size, PEERADDR)
|
|
490
|
+
end
|
|
491
|
+
end
|
|
294
492
|
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
493
|
+
# check emitted data
|
|
494
|
+
emits = d.emits
|
|
495
|
+
assert_equal 16, emits.size
|
|
496
|
+
assert emits.map(&:first).all?{|t| t == "test.tag" }
|
|
497
|
+
assert_equal (0...16).to_a, emits.map{|_tag, t, _record| t - time }
|
|
498
|
+
|
|
499
|
+
# check log
|
|
500
|
+
assert d.instance.log.logs.select{|line|
|
|
501
|
+
line =~ / \[warn\]: Input chunk size is larger than 'chunk_size_warn_limit':/ &&
|
|
502
|
+
line =~ / tag="test.tag" source="host: 127.0.0.1, addr: 127.0.0.1, port: \d+" limit=16777216 size=16777501/
|
|
503
|
+
}.size == 1, "large chunk warning is not logged"
|
|
504
|
+
end
|
|
301
505
|
|
|
302
|
-
|
|
303
|
-
|
|
506
|
+
def test_send_large_chunk_only_warning
|
|
507
|
+
d = create_driver(CONFIG + %[
|
|
304
508
|
chunk_size_warn_limit 16M
|
|
305
509
|
])
|
|
306
|
-
|
|
510
|
+
time = Fluent::EventTime.parse("2014-04-25 13:14:15 UTC")
|
|
307
511
|
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
512
|
+
# generate over 16M chunk
|
|
513
|
+
str = "X" * 1024 * 1024
|
|
514
|
+
chunk = [ "test.tag", (0...16).map{|i| [time + i, {"data" => str}] } ].to_msgpack
|
|
311
515
|
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
516
|
+
d.run do
|
|
517
|
+
sleep 0.1 until d.instance.instance_eval{ @thread } && d.instance.instance_eval{ @thread }.status
|
|
518
|
+
|
|
519
|
+
Fluent::Engine.msgpack_factory.unpacker.feed_each(chunk) do |obj|
|
|
520
|
+
d.instance.send(:on_message, obj, chunk.size, PEERADDR)
|
|
521
|
+
end
|
|
315
522
|
end
|
|
316
|
-
end
|
|
317
523
|
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
524
|
+
# check log
|
|
525
|
+
assert d.instance.log.logs.select{ |line|
|
|
526
|
+
line =~ / \[warn\]: Input chunk size is larger than 'chunk_size_warn_limit':/ &&
|
|
527
|
+
line =~ / tag="test.tag" source="host: 127.0.0.1, addr: 127.0.0.1, port: \d+" limit=16777216 size=16777501/
|
|
528
|
+
}.size == 1, "large chunk warning is not logged"
|
|
529
|
+
end
|
|
324
530
|
|
|
325
|
-
|
|
326
|
-
|
|
531
|
+
def test_send_large_chunk_limit
|
|
532
|
+
d = create_driver(CONFIG + %[
|
|
327
533
|
chunk_size_warn_limit 16M
|
|
328
534
|
chunk_size_limit 32M
|
|
329
535
|
])
|
|
330
536
|
|
|
331
|
-
|
|
537
|
+
time = Fluent::EventTime.parse("2014-04-25 13:14:15 UTC")
|
|
332
538
|
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
539
|
+
# generate over 32M chunk
|
|
540
|
+
str = "X" * 1024 * 1024
|
|
541
|
+
chunk = [ "test.tag", (0...32).map{|i| [time + i, {"data" => str}] } ].to_msgpack
|
|
542
|
+
assert chunk.size > (32 * 1024 * 1024)
|
|
337
543
|
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
d.instance.send(:on_message, obj, chunk.size, "host: 127.0.0.1, addr: 127.0.0.1, port: 0000")
|
|
342
|
-
end
|
|
343
|
-
end
|
|
344
|
-
|
|
345
|
-
# check emitted data
|
|
346
|
-
emits = d.emits
|
|
347
|
-
assert_equal 0, emits.size
|
|
544
|
+
# d.run => send_data
|
|
545
|
+
d.run do
|
|
546
|
+
sleep 0.1 until d.instance.instance_eval{ @thread } && d.instance.instance_eval{ @thread }.status
|
|
348
547
|
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
}.size == 1, "large chunk warning is not logged"
|
|
354
|
-
end
|
|
548
|
+
Fluent::Engine.msgpack_factory.unpacker.feed_each(chunk) do |obj|
|
|
549
|
+
d.instance.send(:on_message, obj, chunk.size, PEERADDR)
|
|
550
|
+
end
|
|
551
|
+
end
|
|
355
552
|
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
d = create_driver
|
|
553
|
+
# check emitted data
|
|
554
|
+
emits = d.emits
|
|
555
|
+
assert_equal 0, emits.size
|
|
360
556
|
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
557
|
+
# check log
|
|
558
|
+
assert d.instance.log.logs.select{|line|
|
|
559
|
+
line =~ / \[warn\]: Input chunk size is larger than 'chunk_size_limit', dropped:/ &&
|
|
560
|
+
line =~ / tag="test.tag" source="host: 127.0.0.1, addr: 127.0.0.1, port: \d+" limit=33554432 size=33554989/
|
|
561
|
+
}.size == 1, "large chunk warning is not logged"
|
|
364
562
|
end
|
|
365
563
|
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
# check log
|
|
371
|
-
assert d.instance.log.logs.select{|line|
|
|
372
|
-
line =~ / \[warn\]: incoming chunk is broken: source="host: 127.0.0.1, addr: 127.0.0.1, port: \d+" msg=#{data.inspect}/
|
|
373
|
-
}.size == 1, "should not accept broken chunk"
|
|
374
|
-
end
|
|
564
|
+
data('string chunk' => 'broken string',
|
|
565
|
+
'integer chunk' => 10)
|
|
566
|
+
def test_send_broken_chunk(data)
|
|
567
|
+
d = create_driver
|
|
375
568
|
|
|
376
|
-
|
|
377
|
-
|
|
569
|
+
# d.run => send_data
|
|
570
|
+
d.run do
|
|
571
|
+
sleep 0.1 until d.instance.instance_eval{ @thread } && d.instance.instance_eval{ @thread }.status
|
|
378
572
|
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
events = [
|
|
382
|
-
["tag1", time, {"a"=>1}],
|
|
383
|
-
["tag2", time, {"a"=>2}]
|
|
384
|
-
]
|
|
385
|
-
d.expected_emits_length = events.length
|
|
573
|
+
d.instance.send(:on_message, data, 1000000000, PEERADDR)
|
|
574
|
+
end
|
|
386
575
|
|
|
387
|
-
|
|
576
|
+
# check emitted data
|
|
577
|
+
emits = d.emits
|
|
578
|
+
assert_equal 0, emits.size
|
|
388
579
|
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
send_data [tag, _time, record, op].to_msgpack, true
|
|
394
|
-
}
|
|
580
|
+
# check log
|
|
581
|
+
assert d.instance.log.logs.select{|line|
|
|
582
|
+
line =~ / \[warn\]: incoming chunk is broken: source="host: 127.0.0.1, addr: 127.0.0.1, port: \d+" msg=#{data.inspect}/
|
|
583
|
+
}.size == 1, "should not accept broken chunk"
|
|
395
584
|
end
|
|
396
|
-
|
|
397
|
-
assert_equal events, d.emits
|
|
398
|
-
assert_equal expected_acks, @responses.map { |res| MessagePack.unpack(res)['ack'] }
|
|
399
585
|
end
|
|
400
586
|
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
587
|
+
class RespondToRequiringAck < self
|
|
588
|
+
extend Fluent::Test::StartupShutdown
|
|
589
|
+
|
|
590
|
+
data(tcp: {
|
|
591
|
+
config: CONFIG,
|
|
592
|
+
options: {
|
|
593
|
+
auth: false
|
|
594
|
+
}
|
|
595
|
+
},
|
|
596
|
+
auth: {
|
|
597
|
+
config: CONFIG_AUTH,
|
|
598
|
+
options: {
|
|
599
|
+
auth: true
|
|
600
|
+
}
|
|
601
|
+
})
|
|
602
|
+
def test_message(data)
|
|
603
|
+
config = data[:config]
|
|
604
|
+
options = data[:options]
|
|
605
|
+
d = create_driver(config)
|
|
606
|
+
|
|
607
|
+
time = Fluent::EventTime.parse("2011-01-02 13:14:15 UTC")
|
|
608
|
+
|
|
609
|
+
events = [
|
|
610
|
+
["tag1", time, {"a"=>1}],
|
|
611
|
+
["tag2", time, {"a"=>2}]
|
|
612
|
+
]
|
|
613
|
+
d.expected_emits_length = events.length
|
|
614
|
+
|
|
615
|
+
expected_acks = []
|
|
616
|
+
|
|
617
|
+
d.run do
|
|
618
|
+
sleep 0.1 until d.instance.instance_eval{ @thread } && d.instance.instance_eval{ @thread }.status
|
|
619
|
+
|
|
620
|
+
events.each {|tag, _time, record|
|
|
621
|
+
op = { 'chunk' => Base64.encode64(record.object_id.to_s) }
|
|
622
|
+
expected_acks << op['chunk']
|
|
623
|
+
send_data [tag, _time, record, op].to_msgpack, try_to_receive_response: true, **options
|
|
624
|
+
}
|
|
625
|
+
end
|
|
404
626
|
|
|
405
|
-
|
|
627
|
+
assert_equal events, d.emits
|
|
628
|
+
assert_equal expected_acks, @responses.map { |res| MessagePack.unpack(res)['ack'] }
|
|
406
629
|
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
630
|
+
sleep 0.1 while d.instance.instance_eval{ @thread }.status # to confirm that plugin stopped completely
|
|
631
|
+
end
|
|
632
|
+
|
|
633
|
+
# FIX: response is not pushed into @responses because IO.select has been blocked until InputForward shutdowns
|
|
634
|
+
data(tcp: {
|
|
635
|
+
config: CONFIG,
|
|
636
|
+
options: {
|
|
637
|
+
auth: false
|
|
638
|
+
}
|
|
639
|
+
},
|
|
640
|
+
auth: {
|
|
641
|
+
config: CONFIG_AUTH,
|
|
642
|
+
options: {
|
|
643
|
+
auth: true
|
|
644
|
+
}
|
|
645
|
+
})
|
|
646
|
+
def test_forward(data)
|
|
647
|
+
config = data[:config]
|
|
648
|
+
options = data[:options]
|
|
649
|
+
d = create_driver(config)
|
|
650
|
+
|
|
651
|
+
time = Fluent::EventTime.parse("2011-01-02 13:14:15 UTC")
|
|
652
|
+
|
|
653
|
+
events = [
|
|
654
|
+
["tag1", time, {"a"=>1}],
|
|
655
|
+
["tag1", time, {"a"=>2}]
|
|
656
|
+
]
|
|
657
|
+
d.expected_emits_length = events.length
|
|
658
|
+
|
|
659
|
+
expected_acks = []
|
|
660
|
+
|
|
661
|
+
d.run do
|
|
662
|
+
sleep 0.1 until d.instance.instance_eval{ @thread } && d.instance.instance_eval{ @thread }.status
|
|
663
|
+
|
|
664
|
+
entries = []
|
|
665
|
+
events.each {|_tag, _time, record|
|
|
666
|
+
entries << [_time, record]
|
|
667
|
+
}
|
|
668
|
+
op = { 'chunk' => Base64.encode64(entries.object_id.to_s) }
|
|
669
|
+
expected_acks << op['chunk']
|
|
670
|
+
send_data ["tag1", entries, op].to_msgpack, try_to_receive_response: true, **options
|
|
671
|
+
end
|
|
412
672
|
|
|
413
|
-
|
|
673
|
+
assert_equal events, d.emits
|
|
674
|
+
assert_equal expected_acks, @responses.map { |res| MessagePack.unpack(res)['ack'] }
|
|
414
675
|
|
|
415
|
-
|
|
416
|
-
entries = []
|
|
417
|
-
events.each {|_tag, _time, record|
|
|
418
|
-
entries << [time, record]
|
|
419
|
-
}
|
|
420
|
-
op = { 'chunk' => Base64.encode64(entries.object_id.to_s) }
|
|
421
|
-
expected_acks << op['chunk']
|
|
422
|
-
send_data ["tag1", entries, op].to_msgpack, true
|
|
676
|
+
sleep 0.1 while d.instance.instance_eval{ @thread }.status # to confirm that plugin stopped completely
|
|
423
677
|
end
|
|
424
678
|
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
679
|
+
data(tcp: {
|
|
680
|
+
config: CONFIG,
|
|
681
|
+
options: {
|
|
682
|
+
auth: false
|
|
683
|
+
}
|
|
684
|
+
},
|
|
685
|
+
auth: {
|
|
686
|
+
config: CONFIG_AUTH,
|
|
687
|
+
options: {
|
|
688
|
+
auth: true
|
|
689
|
+
}
|
|
690
|
+
})
|
|
691
|
+
def test_packed_forward
|
|
692
|
+
config = data[:config]
|
|
693
|
+
options = data[:options]
|
|
694
|
+
d = create_driver(config)
|
|
695
|
+
|
|
696
|
+
time = Fluent::EventTime.parse("2011-01-02 13:14:15 UTC")
|
|
697
|
+
|
|
698
|
+
events = [
|
|
699
|
+
["tag1", time, {"a"=>1}],
|
|
700
|
+
["tag1", time, {"a"=>2}]
|
|
701
|
+
]
|
|
702
|
+
d.expected_emits_length = events.length
|
|
703
|
+
|
|
704
|
+
expected_acks = []
|
|
705
|
+
|
|
706
|
+
d.run do
|
|
707
|
+
sleep 0.1 until d.instance.instance_eval{ @thread } && d.instance.instance_eval{ @thread }.status
|
|
708
|
+
|
|
709
|
+
entries = ''
|
|
710
|
+
events.each {|_tag, _time,record|
|
|
711
|
+
[time, record].to_msgpack(entries)
|
|
712
|
+
}
|
|
713
|
+
op = { 'chunk' => Base64.encode64(entries.object_id.to_s) }
|
|
714
|
+
expected_acks << op['chunk']
|
|
715
|
+
send_data ["tag1", entries, op].to_msgpack, try_to_receive_response: true, **options
|
|
716
|
+
end
|
|
428
717
|
|
|
429
|
-
|
|
430
|
-
|
|
718
|
+
assert_equal events, d.emits
|
|
719
|
+
assert_equal expected_acks, @responses.map { |res| MessagePack.unpack(res)['ack'] }
|
|
431
720
|
|
|
432
|
-
|
|
721
|
+
sleep 0.1 while d.instance.instance_eval{ @thread }.status # to confirm that plugin stopped completely
|
|
722
|
+
end
|
|
433
723
|
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
724
|
+
data(tcp: {
|
|
725
|
+
config: CONFIG,
|
|
726
|
+
options: {
|
|
727
|
+
auth: false
|
|
728
|
+
}
|
|
729
|
+
},
|
|
730
|
+
auth: {
|
|
731
|
+
config: CONFIG_AUTH,
|
|
732
|
+
options: {
|
|
733
|
+
auth: true
|
|
734
|
+
}
|
|
735
|
+
})
|
|
736
|
+
def test_message_json(data)
|
|
737
|
+
config = data[:config]
|
|
738
|
+
options = data[:options]
|
|
739
|
+
omit "with json, auth doen NOT work" if options[:auth]
|
|
740
|
+
d = create_driver(config)
|
|
741
|
+
|
|
742
|
+
time = Time.parse("2011-01-02 13:14:15 UTC").to_i
|
|
743
|
+
|
|
744
|
+
events = [
|
|
745
|
+
["tag1", time, {"a"=>1}],
|
|
746
|
+
["tag2", time, {"a"=>2}]
|
|
747
|
+
]
|
|
748
|
+
d.expected_emits_length = events.length
|
|
749
|
+
|
|
750
|
+
expected_acks = []
|
|
751
|
+
|
|
752
|
+
d.expected_emits_length = events.length
|
|
753
|
+
d.run_timeout = 2
|
|
754
|
+
d.run do
|
|
755
|
+
sleep 0.1 until d.instance.instance_eval{ @thread } && d.instance.instance_eval{ @thread }.status
|
|
756
|
+
|
|
757
|
+
events.each {|tag, _time, record|
|
|
758
|
+
op = { 'chunk' => Base64.encode64(record.object_id.to_s) }
|
|
759
|
+
expected_acks << op['chunk']
|
|
760
|
+
send_data [tag, _time, record, op].to_json, try_to_receive_response: true, **options
|
|
761
|
+
}
|
|
762
|
+
end
|
|
439
763
|
|
|
440
|
-
|
|
764
|
+
assert_equal events, d.emits
|
|
765
|
+
assert_equal expected_acks, @responses.map { |res| JSON.parse(res)['ack'] }
|
|
441
766
|
|
|
442
|
-
|
|
443
|
-
entries = ''
|
|
444
|
-
events.each {|_tag, _time, record|
|
|
445
|
-
[_time, record].to_msgpack(entries)
|
|
446
|
-
}
|
|
447
|
-
op = { 'chunk' => Base64.encode64(entries.object_id.to_s) }
|
|
448
|
-
expected_acks << op['chunk']
|
|
449
|
-
send_data ["tag1", entries, op].to_msgpack, true
|
|
767
|
+
sleep 0.1 while d.instance.instance_eval{ @thread }.status # to confirm that plugin stopped completely
|
|
450
768
|
end
|
|
451
|
-
|
|
452
|
-
assert_equal events, d.emits
|
|
453
|
-
assert_equal expected_acks, @responses.map { |res| MessagePack.unpack(res)['ack'] }
|
|
454
769
|
end
|
|
455
770
|
|
|
456
|
-
|
|
457
|
-
|
|
771
|
+
class NotRespondToNotRequiringAck < self
|
|
772
|
+
extend Fluent::Test::StartupShutdown
|
|
773
|
+
|
|
774
|
+
data(tcp: {
|
|
775
|
+
config: CONFIG,
|
|
776
|
+
options: {
|
|
777
|
+
auth: false
|
|
778
|
+
}
|
|
779
|
+
},
|
|
780
|
+
auth: {
|
|
781
|
+
config: CONFIG_AUTH,
|
|
782
|
+
options: {
|
|
783
|
+
auth: true
|
|
784
|
+
}
|
|
785
|
+
})
|
|
786
|
+
def test_message
|
|
787
|
+
config = data[:config]
|
|
788
|
+
options = data[:options]
|
|
789
|
+
d = create_driver(config)
|
|
790
|
+
|
|
791
|
+
time = Fluent::EventTime.parse("2011-01-02 13:14:15 UTC")
|
|
792
|
+
|
|
793
|
+
events = [
|
|
794
|
+
["tag1", time, {"a"=>1}],
|
|
795
|
+
["tag2", time, {"a"=>2}]
|
|
796
|
+
]
|
|
797
|
+
d.expected_emits_length = events.length
|
|
798
|
+
|
|
799
|
+
d.run do
|
|
800
|
+
sleep 0.1 until d.instance.instance_eval{ @thread } && d.instance.instance_eval{ @thread }.status
|
|
801
|
+
|
|
802
|
+
events.each {|tag, _time, record|
|
|
803
|
+
send_data [tag, _time, record].to_msgpack, try_to_receive_response: true, **options
|
|
804
|
+
}
|
|
805
|
+
end
|
|
458
806
|
|
|
459
|
-
|
|
807
|
+
assert_equal events, d.emits
|
|
808
|
+
assert_equal ["", ""], @responses
|
|
460
809
|
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
["tag2", time, {"a"=>2}]
|
|
464
|
-
]
|
|
465
|
-
d.expected_emits_length = events.length
|
|
810
|
+
sleep 0.1 while d.instance.instance_eval{ @thread }.status # to confirm that plugin stopped completely
|
|
811
|
+
end
|
|
466
812
|
|
|
467
|
-
|
|
813
|
+
data(tcp: {
|
|
814
|
+
config: CONFIG,
|
|
815
|
+
options: {
|
|
816
|
+
auth: false
|
|
817
|
+
}
|
|
818
|
+
},
|
|
819
|
+
auth: {
|
|
820
|
+
config: CONFIG_AUTH,
|
|
821
|
+
options: {
|
|
822
|
+
auth: true
|
|
823
|
+
}
|
|
824
|
+
})
|
|
825
|
+
def test_forward(data)
|
|
826
|
+
config = data[:config]
|
|
827
|
+
options = data[:options]
|
|
828
|
+
d = create_driver(config)
|
|
829
|
+
|
|
830
|
+
time = Fluent::EventTime.parse("2011-01-02 13:14:15 UTC")
|
|
831
|
+
|
|
832
|
+
events = [
|
|
833
|
+
["tag1", time, {"a"=>1}],
|
|
834
|
+
["tag1", time, {"a"=>2}]
|
|
835
|
+
]
|
|
836
|
+
d.expected_emits_length = events.length
|
|
837
|
+
|
|
838
|
+
d.run do
|
|
839
|
+
sleep 0.1 until d.instance.instance_eval{ @thread } && d.instance.instance_eval{ @thread }.status
|
|
840
|
+
|
|
841
|
+
entries = []
|
|
842
|
+
events.each {|tag, _time, record|
|
|
843
|
+
entries << [_time, record]
|
|
844
|
+
}
|
|
845
|
+
send_data ["tag1", entries].to_msgpack, try_to_receive_response: true, **options
|
|
846
|
+
end
|
|
468
847
|
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
send_data [tag, _time, record, op].to_json, true
|
|
474
|
-
}
|
|
848
|
+
assert_equal events, d.emits
|
|
849
|
+
assert_equal [nil], @responses
|
|
850
|
+
|
|
851
|
+
sleep 0.1 while d.instance.instance_eval{ @thread }.status # to confirm that plugin stopped completely
|
|
475
852
|
end
|
|
476
853
|
|
|
477
|
-
|
|
478
|
-
|
|
854
|
+
data(tcp: {
|
|
855
|
+
config: CONFIG,
|
|
856
|
+
options: {
|
|
857
|
+
auth: false
|
|
858
|
+
}
|
|
859
|
+
},
|
|
860
|
+
auth: {
|
|
861
|
+
config: CONFIG_AUTH,
|
|
862
|
+
options: {
|
|
863
|
+
auth: true
|
|
864
|
+
}
|
|
865
|
+
})
|
|
866
|
+
def test_packed_forward(data)
|
|
867
|
+
config = data[:config]
|
|
868
|
+
options = data[:options]
|
|
869
|
+
d = create_driver(config)
|
|
870
|
+
|
|
871
|
+
time = Fluent::EventTime.parse("2011-01-02 13:14:15 UTC")
|
|
872
|
+
|
|
873
|
+
events = [
|
|
874
|
+
["tag1", time, {"a"=>1}],
|
|
875
|
+
["tag1", time, {"a"=>2}]
|
|
876
|
+
]
|
|
877
|
+
d.expected_emits_length = events.length
|
|
878
|
+
|
|
879
|
+
d.run do
|
|
880
|
+
sleep 0.1 until d.instance.instance_eval{ @thread } && d.instance.instance_eval{ @thread }.status
|
|
881
|
+
|
|
882
|
+
entries = ''
|
|
883
|
+
events.each {|tag, _time, record|
|
|
884
|
+
[_time, record].to_msgpack(entries)
|
|
885
|
+
}
|
|
886
|
+
send_data ["tag1", entries].to_msgpack, try_to_receive_response: true, **options
|
|
887
|
+
end
|
|
479
888
|
|
|
480
|
-
|
|
889
|
+
assert_equal events, d.emits
|
|
890
|
+
assert_equal [nil], @responses
|
|
481
891
|
|
|
482
|
-
|
|
483
|
-
|
|
892
|
+
sleep 0.1 while d.instance.instance_eval{ @thread }.status # to confirm that plugin stopped completely
|
|
893
|
+
end
|
|
484
894
|
|
|
485
|
-
|
|
895
|
+
data(tcp: {
|
|
896
|
+
config: CONFIG,
|
|
897
|
+
options: {
|
|
898
|
+
auth: false
|
|
899
|
+
}
|
|
900
|
+
},
|
|
901
|
+
auth: {
|
|
902
|
+
config: CONFIG_AUTH,
|
|
903
|
+
options: {
|
|
904
|
+
auth: true
|
|
905
|
+
}
|
|
906
|
+
})
|
|
907
|
+
def test_message_json(data)
|
|
908
|
+
config = data[:config]
|
|
909
|
+
options = data[:options]
|
|
910
|
+
omit "with json, auth doen NOT work" if options[:auth]
|
|
911
|
+
d = create_driver(config)
|
|
912
|
+
|
|
913
|
+
time = Time.parse("2011-01-02 13:14:15 UTC").to_i
|
|
914
|
+
|
|
915
|
+
events = [
|
|
916
|
+
["tag1", time, {"a"=>1}],
|
|
917
|
+
["tag2", time, {"a"=>2}]
|
|
918
|
+
]
|
|
919
|
+
d.expected_emits_length = events.length
|
|
920
|
+
|
|
921
|
+
d.run do
|
|
922
|
+
sleep 0.1 until d.instance.instance_eval{ @thread } && d.instance.instance_eval{ @thread }.status
|
|
923
|
+
|
|
924
|
+
events.each {|tag, _time, record|
|
|
925
|
+
send_data [tag, _time, record].to_json, try_to_receive_response: true, **options
|
|
926
|
+
}
|
|
927
|
+
end
|
|
486
928
|
|
|
487
|
-
|
|
488
|
-
[
|
|
489
|
-
["tag2", time, {"a"=>2}]
|
|
490
|
-
]
|
|
491
|
-
d.expected_emits_length = events.length
|
|
929
|
+
assert_equal events, d.emits
|
|
930
|
+
assert_equal [nil, nil], @responses
|
|
492
931
|
|
|
493
|
-
|
|
494
|
-
events.each {|tag, _time, record|
|
|
495
|
-
send_data [tag, _time, record].to_msgpack, true
|
|
496
|
-
}
|
|
932
|
+
sleep 0.1 while d.instance.instance_eval{ @thread }.status # to confirm that plugin stopped completely
|
|
497
933
|
end
|
|
934
|
+
end
|
|
498
935
|
|
|
499
|
-
|
|
500
|
-
|
|
936
|
+
def packer(*args)
|
|
937
|
+
Fluent::Engine.msgpack_factory.packer(*args)
|
|
501
938
|
end
|
|
502
939
|
|
|
503
|
-
def
|
|
504
|
-
|
|
940
|
+
def unpacker
|
|
941
|
+
Fluent::Engine.msgpack_factory.unpacker
|
|
942
|
+
end
|
|
505
943
|
|
|
506
|
-
|
|
944
|
+
# res
|
|
945
|
+
# '' : socket is disconnected without any data
|
|
946
|
+
# nil: socket read timeout
|
|
947
|
+
def read_data(io, timeout, &block)
|
|
948
|
+
res = ''
|
|
949
|
+
select_timeout = 2
|
|
950
|
+
timeout_at = Time.now + timeout
|
|
951
|
+
begin
|
|
952
|
+
buf = ''
|
|
953
|
+
io_activated = false
|
|
954
|
+
while Time.now < timeout_at
|
|
955
|
+
if IO.select([io], nil, nil, select_timeout)
|
|
956
|
+
io_activated = true
|
|
957
|
+
buf = io.readpartial(2048)
|
|
958
|
+
res ||= ''
|
|
959
|
+
res << buf
|
|
960
|
+
|
|
961
|
+
break if block.call(res)
|
|
962
|
+
end
|
|
963
|
+
end
|
|
964
|
+
res = nil unless io_activated # timeout without no data arrival
|
|
965
|
+
rescue Errno::EAGAIN
|
|
966
|
+
sleep 0.01
|
|
967
|
+
retry if res == ''
|
|
968
|
+
# if res is not empty, all data in socket buffer are read, so do not retry
|
|
969
|
+
rescue IOError, EOFError, Errno::ECONNRESET
|
|
970
|
+
# socket disconnected
|
|
971
|
+
end
|
|
972
|
+
res
|
|
973
|
+
end
|
|
507
974
|
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
975
|
+
def simulate_auth_sequence(io, shared_key=SHARED_KEY, username=USER_NAME, password=USER_PASSWORD)
|
|
976
|
+
auth_response_timeout = 30
|
|
977
|
+
shared_key_salt = 'salt'
|
|
978
|
+
|
|
979
|
+
# reading helo
|
|
980
|
+
helo_data = read_data(io, auth_response_timeout){|data| MessagePack.unpack(data) rescue nil }
|
|
981
|
+
raise "Authentication packet timeout" unless helo_data
|
|
982
|
+
raise "Authentication connection closed" if helo_data == ''
|
|
983
|
+
# ['HELO', options(hash)]
|
|
984
|
+
helo = MessagePack.unpack(helo_data)
|
|
985
|
+
raise "Invalid HELO header" unless helo[0] == 'HELO'
|
|
986
|
+
raise "Invalid HELO option object" unless helo[1].is_a?(Hash)
|
|
987
|
+
@options = helo[1]
|
|
988
|
+
|
|
989
|
+
# sending ping
|
|
990
|
+
ping = [
|
|
991
|
+
'PING',
|
|
992
|
+
'selfhostname',
|
|
993
|
+
shared_key_salt,
|
|
994
|
+
Digest::SHA512.new
|
|
995
|
+
.update(shared_key_salt)
|
|
996
|
+
.update('selfhostname')
|
|
997
|
+
.update(@options['nonce'])
|
|
998
|
+
.update(shared_key).hexdigest,
|
|
511
999
|
]
|
|
512
|
-
|
|
1000
|
+
if @options['auth'] # auth enabled -> value is auth salt
|
|
1001
|
+
pass_digest = Digest::SHA512.new.update(@options['auth']).update(username).update(password).hexdigest
|
|
1002
|
+
ping.push(username, pass_digest)
|
|
1003
|
+
else
|
|
1004
|
+
ping.push('', '')
|
|
1005
|
+
end
|
|
1006
|
+
io.write ping.to_msgpack
|
|
1007
|
+
io.flush
|
|
1008
|
+
|
|
1009
|
+
# reading pong
|
|
1010
|
+
pong_data = read_data(io, auth_response_timeout){|data| MessagePack.unpack(data) rescue nil }
|
|
1011
|
+
raise "PONG packet timeout" unless pong_data
|
|
1012
|
+
raise "PONG connection closed" if pong_data == ''
|
|
1013
|
+
# ['PING', bool(auth_result), string(reason_if_failed), self_hostname, shared_key_digest]
|
|
1014
|
+
pong = MessagePack.unpack(pong_data)
|
|
1015
|
+
raise "Invalid PONG header" unless pong[0] == 'PONG'
|
|
1016
|
+
raise "Authentication Failure: #{pong[2]}" unless pong[1]
|
|
1017
|
+
clientside_calculated = Digest::SHA512.new
|
|
1018
|
+
.update(shared_key_salt)
|
|
1019
|
+
.update(pong[3])
|
|
1020
|
+
.update(@options['nonce'])
|
|
1021
|
+
.update(shared_key).hexdigest
|
|
1022
|
+
raise "Shared key digest mismatch" unless clientside_calculated == pong[4]
|
|
1023
|
+
|
|
1024
|
+
# authentication success
|
|
1025
|
+
true
|
|
1026
|
+
end
|
|
513
1027
|
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
1028
|
+
# Data ordering is not assured:
|
|
1029
|
+
# Records in different sockets are processed on different thread, so its scheduling make effect
|
|
1030
|
+
# on order of emitted records.
|
|
1031
|
+
# So, we MUST sort emitted records in different `send_data` before assertion.
|
|
1032
|
+
def send_data(data, try_to_receive_response: false, response_timeout: 5, auth: false)
|
|
1033
|
+
io = connect
|
|
1034
|
+
|
|
1035
|
+
if auth
|
|
1036
|
+
simulate_auth_sequence(io)
|
|
520
1037
|
end
|
|
521
1038
|
|
|
522
|
-
|
|
523
|
-
|
|
1039
|
+
io.write data
|
|
1040
|
+
io.flush
|
|
1041
|
+
if try_to_receive_response
|
|
1042
|
+
@responses << read_data(io, response_timeout){|d| MessagePack.unpack(d) rescue nil }
|
|
1043
|
+
end
|
|
1044
|
+
ensure
|
|
1045
|
+
io.close rescue nil # SSL socket requires any writes to close sockets
|
|
524
1046
|
end
|
|
525
1047
|
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
time = Fluent::EventTime.parse("2011-01-02 13:14:15 UTC")
|
|
1048
|
+
sub_test_case 'source_hostname_key feature' do
|
|
1049
|
+
extend Fluent::Test::StartupShutdown
|
|
530
1050
|
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
1051
|
+
test 'message protocol with source_hostname_key' do
|
|
1052
|
+
execute_test { |events|
|
|
1053
|
+
events.each { |tag, time, record|
|
|
1054
|
+
send_data [tag, time, record].to_msgpack
|
|
1055
|
+
}
|
|
1056
|
+
}
|
|
1057
|
+
end
|
|
536
1058
|
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
1059
|
+
test 'forward protocol with source_hostname_key' do
|
|
1060
|
+
execute_test { |events|
|
|
1061
|
+
entries = []
|
|
1062
|
+
events.each {|tag,time,record|
|
|
1063
|
+
entries << [time, record]
|
|
1064
|
+
}
|
|
1065
|
+
send_data ['tag1', entries].to_msgpack
|
|
541
1066
|
}
|
|
542
|
-
send_data ["tag1", entries].to_msgpack, true
|
|
543
1067
|
end
|
|
544
1068
|
|
|
545
|
-
|
|
546
|
-
|
|
1069
|
+
test 'packed forward protocol with source_hostname_key' do
|
|
1070
|
+
execute_test { |events|
|
|
1071
|
+
entries = ''
|
|
1072
|
+
events.each { |tag, time, record|
|
|
1073
|
+
Fluent::Engine.msgpack_factory.packer(entries).write([time, record]).flush
|
|
1074
|
+
}
|
|
1075
|
+
send_data Fluent::Engine.msgpack_factory.packer.write(["tag1", entries]).to_s
|
|
1076
|
+
}
|
|
1077
|
+
end
|
|
547
1078
|
end
|
|
548
1079
|
|
|
549
|
-
def
|
|
550
|
-
d = create_driver
|
|
551
|
-
|
|
552
|
-
time = Time.parse("2011-01-02 13:14:15 UTC").to_i
|
|
1080
|
+
def execute_test(&block)
|
|
1081
|
+
d = create_driver(CONFIG + 'source_hostname_key source')
|
|
553
1082
|
|
|
1083
|
+
time = Fluent::EventTime.parse("2011-01-02 13:14:15 UTC")
|
|
554
1084
|
events = [
|
|
555
1085
|
["tag1", time, {"a"=>1}],
|
|
556
|
-
["
|
|
1086
|
+
["tag1", time, {"a"=>2}]
|
|
557
1087
|
]
|
|
558
1088
|
d.expected_emits_length = events.length
|
|
559
1089
|
|
|
560
1090
|
d.run do
|
|
561
|
-
events
|
|
562
|
-
send_data [tag, _time, record].to_json, true
|
|
563
|
-
}
|
|
1091
|
+
block.call(events)
|
|
564
1092
|
end
|
|
565
1093
|
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
def send_data(data, try_to_receive_response=false, response_timeout=1)
|
|
571
|
-
io = connect
|
|
572
|
-
begin
|
|
573
|
-
io.write data
|
|
574
|
-
if try_to_receive_response
|
|
575
|
-
if IO.select([io], nil, nil, response_timeout)
|
|
576
|
-
res = io.recv(1024)
|
|
577
|
-
end
|
|
578
|
-
# timeout means no response, so push nil to @responses
|
|
579
|
-
end
|
|
580
|
-
ensure
|
|
581
|
-
io.close
|
|
582
|
-
end
|
|
583
|
-
@responses << res if try_to_receive_response
|
|
1094
|
+
d.emits.each { |tag, _time, record|
|
|
1095
|
+
assert_true record.has_key?('source')
|
|
1096
|
+
}
|
|
584
1097
|
end
|
|
585
1098
|
|
|
586
1099
|
# TODO heartbeat
|