fluentd 0.12.0.pre.1 → 0.12.0.pre.2

Sign up to get free protection for your applications and to get access to all the features.

Potentially problematic release.


This version of fluentd might be problematic. Click here for more details.

Files changed (81) hide show
  1. checksums.yaml +4 -4
  2. data/.gitignore +1 -1
  3. data/.travis.yml +1 -0
  4. data/ChangeLog +21 -0
  5. data/README.md +10 -2
  6. data/Rakefile +4 -13
  7. data/example/v1_literal_example.conf +36 -0
  8. data/fluentd.gemspec +4 -1
  9. data/lib/fluent/buffer.rb +73 -46
  10. data/lib/fluent/command/fluentd.rb +7 -2
  11. data/lib/fluent/config/basic_parser.rb +5 -0
  12. data/lib/fluent/config/element.rb +2 -5
  13. data/lib/fluent/config/literal_parser.rb +26 -7
  14. data/lib/fluent/config/section.rb +2 -0
  15. data/lib/fluent/config/v1_parser.rb +9 -2
  16. data/lib/fluent/formatter.rb +2 -1
  17. data/lib/fluent/mixin.rb +22 -7
  18. data/lib/fluent/output.rb +17 -8
  19. data/lib/fluent/parser.rb +14 -3
  20. data/lib/fluent/plugin/buf_file.rb +30 -15
  21. data/lib/fluent/plugin/filter_grep.rb +69 -0
  22. data/lib/fluent/plugin/filter_record_transformer.rb +183 -0
  23. data/lib/fluent/plugin/in_exec.rb +6 -0
  24. data/lib/fluent/plugin/in_forward.rb +34 -4
  25. data/lib/fluent/plugin/in_http.rb +1 -1
  26. data/lib/fluent/plugin/out_exec.rb +1 -1
  27. data/lib/fluent/plugin/out_exec_filter.rb +8 -1
  28. data/lib/fluent/plugin/out_forward.rb +82 -4
  29. data/lib/fluent/supervisor.rb +1 -1
  30. data/lib/fluent/timezone.rb +131 -0
  31. data/lib/fluent/version.rb +1 -1
  32. data/test/config/assertions.rb +42 -0
  33. data/test/config/test_config_parser.rb +385 -0
  34. data/test/config/test_configurable.rb +530 -0
  35. data/test/config/test_configure_proxy.rb +99 -0
  36. data/test/config/test_dsl.rb +237 -0
  37. data/test/config/test_literal_parser.rb +293 -0
  38. data/test/config/test_section.rb +112 -0
  39. data/test/config/test_system_config.rb +49 -0
  40. data/test/helper.rb +25 -0
  41. data/test/plugin/test_buf_file.rb +604 -0
  42. data/test/plugin/test_buf_memory.rb +204 -0
  43. data/test/plugin/test_filter_grep.rb +124 -0
  44. data/test/plugin/test_filter_record_transformer.rb +251 -0
  45. data/test/plugin/test_in_exec.rb +1 -0
  46. data/test/plugin/test_in_forward.rb +205 -2
  47. data/test/plugin/test_in_gc_stat.rb +1 -0
  48. data/test/plugin/test_in_http.rb +58 -2
  49. data/test/plugin/test_in_object_space.rb +1 -0
  50. data/test/plugin/test_in_status.rb +1 -0
  51. data/test/plugin/test_in_stream.rb +1 -1
  52. data/test/plugin/test_in_syslog.rb +1 -1
  53. data/test/plugin/test_in_tail.rb +1 -0
  54. data/test/plugin/test_in_tcp.rb +1 -1
  55. data/test/plugin/test_in_udp.rb +1 -1
  56. data/test/plugin/test_out_copy.rb +1 -0
  57. data/test/plugin/test_out_exec.rb +1 -0
  58. data/test/plugin/test_out_exec_filter.rb +1 -0
  59. data/test/plugin/test_out_file.rb +36 -0
  60. data/test/plugin/test_out_forward.rb +279 -8
  61. data/test/plugin/test_out_roundrobin.rb +1 -0
  62. data/test/plugin/test_out_stdout.rb +1 -0
  63. data/test/plugin/test_out_stream.rb +1 -1
  64. data/test/test_buffer.rb +530 -0
  65. data/test/test_config.rb +1 -1
  66. data/test/test_configdsl.rb +1 -1
  67. data/test/test_formatter.rb +223 -0
  68. data/test/test_match.rb +1 -2
  69. data/test/test_mixin.rb +74 -2
  70. data/test/test_parser.rb +7 -1
  71. metadata +88 -35
  72. data/lib/fluent/plugin/buf_zfile.rb +0 -75
  73. data/spec/config/config_parser_spec.rb +0 -314
  74. data/spec/config/configurable_spec.rb +0 -524
  75. data/spec/config/configure_proxy_spec.rb +0 -96
  76. data/spec/config/dsl_spec.rb +0 -239
  77. data/spec/config/helper.rb +0 -49
  78. data/spec/config/literal_parser_spec.rb +0 -222
  79. data/spec/config/section_spec.rb +0 -97
  80. data/spec/config/system_config_spec.rb +0 -49
  81. data/spec/spec_helper.rb +0 -60
@@ -0,0 +1,69 @@
1
+ module Fluent
2
+ class GrepFilter < Filter
3
+ Fluent::Plugin.register_filter('grep', self)
4
+
5
+ REGEXP_MAX_NUM = 20
6
+
7
+ (1..REGEXP_MAX_NUM).each {|i| config_param :"regexp#{i}", :string, :default => nil }
8
+ (1..REGEXP_MAX_NUM).each {|i| config_param :"exclude#{i}", :string, :default => nil }
9
+
10
+ # for test
11
+ attr_reader :regexps
12
+ attr_reader :excludes
13
+
14
+ def configure(conf)
15
+ super
16
+
17
+ @regexps = {}
18
+ (1..REGEXP_MAX_NUM).each do |i|
19
+ next unless conf["regexp#{i}"]
20
+ key, regexp = conf["regexp#{i}"].split(/ /, 2)
21
+ raise ConfigError, "regexp#{i} does not contain 2 parameters" unless regexp
22
+ raise ConfigError, "regexp#{i} contains a duplicated key, #{key}" if @regexps[key]
23
+ @regexps[key] = Regexp.compile(regexp)
24
+ end
25
+
26
+ @excludes = {}
27
+ (1..REGEXP_MAX_NUM).each do |i|
28
+ next unless conf["exclude#{i}"]
29
+ key, exclude = conf["exclude#{i}"].split(/ /, 2)
30
+ raise ConfigError, "exclude#{i} does not contain 2 parameters" unless exclude
31
+ raise ConfigError, "exclude#{i} contains a duplicated key, #{key}" if @excludes[key]
32
+ @excludes[key] = Regexp.compile(exclude)
33
+ end
34
+ end
35
+
36
+ def filter_stream(tag, es)
37
+ result_es = MultiEventStream.new
38
+ es.each do |time, record|
39
+ catch(:break_loop) do
40
+ @regexps.each do |key, regexp|
41
+ throw :break_loop unless match(regexp, record[key].to_s)
42
+ end
43
+ @excludes.each do |key, exclude|
44
+ throw :break_loop if match(exclude, record[key].to_s)
45
+ end
46
+ result_es.add(time, record)
47
+ end
48
+ end
49
+ result_es
50
+ rescue => e
51
+ log.warn "failed to grep events", :error_class => e.class, :error => e.message
52
+ log.warn_backtrace
53
+ end
54
+
55
+ private
56
+
57
+ def match(regexp, string)
58
+ begin
59
+ return regexp.match(string)
60
+ rescue ArgumentError => e
61
+ raise e unless e.message.index("invalid byte sequence in".freeze).zero?
62
+ log.info "invalid byte sequence is replaced in `#{string}`"
63
+ string = string.scrub('?')
64
+ retry
65
+ end
66
+ return true
67
+ end
68
+ end
69
+ end
@@ -0,0 +1,183 @@
1
+ require 'ostruct'
2
+
3
+ module Fluent
4
+ class RecordTransformerFilter < Filter
5
+ Fluent::Plugin.register_filter('record_transformer', self)
6
+
7
+ def initialize
8
+ require 'socket'
9
+ super
10
+ end
11
+
12
+ config_param :remove_keys, :string, :default => nil
13
+ config_param :keep_keys, :string, :default => nil
14
+ config_param :renew_record, :bool, :default => false
15
+ config_param :enable_ruby, :bool, :default => false
16
+
17
+ def configure(conf)
18
+ super
19
+
20
+ @map = {}
21
+ # <record></record> directive
22
+ conf.elements.select { |element| element.name == 'record' }.each do |element|
23
+ element.each_pair do |k, v|
24
+ element.has_key?(k) # to suppress unread configuration warning
25
+ @map[k] = v
26
+ end
27
+ end
28
+
29
+ if @remove_keys
30
+ @remove_keys = @remove_keys.split(',')
31
+ end
32
+
33
+ if @keep_keys
34
+ raise Fluent::ConfigError, "`renew_record` must be true to use `keep_keys`" unless @renew_record
35
+ @keep_keys = @keep_keys.split(',')
36
+ end
37
+
38
+ @placeholder_expander =
39
+ if @enable_ruby
40
+ # require utilities which would be used in ruby placeholders
41
+ require 'pathname'
42
+ require 'uri'
43
+ require 'cgi'
44
+ RubyPlaceholderExpander.new(log)
45
+ else
46
+ PlaceholderExpander.new(log)
47
+ end
48
+
49
+ @hostname = Socket.gethostname
50
+ end
51
+
52
+ def filter_stream(tag, es)
53
+ new_es = MultiEventStream.new
54
+ tag_parts = tag.split('.')
55
+ tag_prefix = tag_prefix(tag_parts)
56
+ tag_suffix = tag_suffix(tag_parts)
57
+ placeholders = {
58
+ 'tag' => tag,
59
+ 'tag_parts' => tag_parts,
60
+ 'tag_prefix' => tag_prefix,
61
+ 'tag_suffix' => tag_suffix,
62
+ 'hostname' => @hostname,
63
+ }
64
+ last_record = nil
65
+ es.each do |time, record|
66
+ last_record = record # for debug log
67
+ new_record = reform(time, record, placeholders)
68
+ new_es.add(time, new_record)
69
+ end
70
+ new_es
71
+ rescue => e
72
+ log.warn "failed to reform records", :error_class => e.class, :error => e.message
73
+ log.warn_backtrace
74
+ log.debug "map:#{@map} record:#{last_record} placeholders:#{placeholders}"
75
+ end
76
+
77
+ private
78
+
79
+ def reform(time, record, opts)
80
+ @placeholder_expander.prepare_placeholders(time, record, opts)
81
+
82
+ new_record = @renew_record ? {} : record.dup
83
+ @keep_keys.each {|k| new_record[k] = record[k]} if @keep_keys and @renew_record
84
+ @map.each_pair {|k, v| new_record[k] = @placeholder_expander.expand(v) }
85
+ @remove_keys.each {|k| new_record.delete(k) } if @remove_keys
86
+
87
+ new_record
88
+ end
89
+
90
+ def tag_prefix(tag_parts)
91
+ return [] if tag_parts.empty?
92
+ tag_prefix = [tag_parts.first]
93
+ 1.upto(tag_parts.size-1).each do |i|
94
+ tag_prefix[i] = "#{tag_prefix[i-1]}.#{tag_parts[i]}"
95
+ end
96
+ tag_prefix
97
+ end
98
+
99
+ def tag_suffix(tag_parts)
100
+ return [] if tag_parts.empty?
101
+ rev_tag_parts = tag_parts.reverse
102
+ rev_tag_suffix = [rev_tag_parts.first]
103
+ 1.upto(tag_parts.size-1).each do |i|
104
+ rev_tag_suffix[i] = "#{rev_tag_parts[i]}.#{rev_tag_suffix[i-1]}"
105
+ end
106
+ rev_tag_suffix.reverse!
107
+ end
108
+
109
+ class PlaceholderExpander
110
+ attr_reader :placeholders, :log
111
+
112
+ def initialize(log)
113
+ @log = log
114
+ end
115
+
116
+ def prepare_placeholders(time, record, opts)
117
+ placeholders = { '${time}' => Time.at(time).to_s }
118
+ record.each {|key, value| placeholders.store("${#{key}}", value) }
119
+
120
+ opts.each do |key, value|
121
+ if value.kind_of?(Array) # tag_parts, etc
122
+ size = value.size
123
+ value.each_with_index { |v, idx|
124
+ placeholders.store("${#{key}[#{idx}]}", v)
125
+ placeholders.store("${#{key}[#{idx-size}]}", v) # support [-1]
126
+ }
127
+ else # string, interger, float, and others?
128
+ placeholders.store("${#{key}}", value)
129
+ end
130
+ end
131
+
132
+ @placeholders = placeholders
133
+ end
134
+
135
+ def expand(str)
136
+ str.gsub(/(\${[a-zA-Z0-9_]+(\[-?[0-9]+\])?}|__[A-Z_]+__)/) {
137
+ log.warn "unknown placeholder `#{$1}` found" unless @placeholders.include?($1)
138
+ @placeholders[$1]
139
+ }
140
+ end
141
+ end
142
+
143
+ class RubyPlaceholderExpander
144
+ attr_reader :placeholders, :log
145
+
146
+ def initialize(log)
147
+ @log = log
148
+ end
149
+
150
+ # Get placeholders as a struct
151
+ #
152
+ # @param [Time] time the time
153
+ # @param [Hash] record the record
154
+ # @param [Hash] opts others
155
+ def prepare_placeholders(time, record, opts)
156
+ struct = UndefOpenStruct.new(record)
157
+ struct.time = Time.at(time)
158
+ opts.each {|key, value| struct.__send__("#{key}=", value) }
159
+ @placeholders = struct
160
+ end
161
+
162
+ # Replace placeholders in a string
163
+ #
164
+ # @param [String] str the string to be replaced
165
+ def expand(str)
166
+ interpolated = str.gsub(/\$\{([^}]+)\}/, '#{\1}') # ${..} => #{..}
167
+ begin
168
+ eval "\"#{interpolated}\"", @placeholders.instance_eval { binding }
169
+ rescue => e
170
+ log.warn "failed to expand `#{str}`", :error_class => e.class, :error => e.message
171
+ log.warn_backtrace
172
+ nil
173
+ end
174
+ end
175
+
176
+ class UndefOpenStruct < OpenStruct
177
+ (Object.instance_methods).each do |m|
178
+ undef_method m unless m.to_s =~ /^__|respond_to_missing\?|object_id|public_methods|instance_eval|method_missing|define_singleton_method|respond_to\?|new_ostruct_member/
179
+ end
180
+ end
181
+ end
182
+ end
183
+ end
@@ -21,6 +21,7 @@ module Fluent
21
21
  def initialize
22
22
  super
23
23
  require 'fluent/plugin/exec_util'
24
+ require 'fluent/timezone'
24
25
  end
25
26
 
26
27
  SUPPORTED_FORMAT = {
@@ -53,6 +54,11 @@ module Fluent
53
54
  @localtime = false
54
55
  end
55
56
 
57
+ if conf['timezone']
58
+ @timezone = conf['timezone']
59
+ Fluent::Timezone.validate!(@timezone)
60
+ end
61
+
56
62
  if !@tag && !@tag_key
57
63
  raise ConfigError, "'tag' or 'tag_key' option is required on exec input"
58
64
  end
@@ -54,7 +54,16 @@ module Fluent
54
54
  end
55
55
 
56
56
  def shutdown
57
- @loop.watchers.each {|w| w.detach }
57
+ # In test cases it occasionally appeared that when detaching a watcher, another watcher is also detached.
58
+ # In the case in the iteration of watchers, a watcher that has been already detached is intended to be detached
59
+ # and therfore RuntimeError occurs saying that it is not attached to a loop.
60
+ # It occures only when testing for sending responses to ForwardOutput.
61
+ # Sending responses needs to write the socket that is previously used only to read
62
+ # and a handler has 2 watchers that is used to read and to write.
63
+ # This problem occurs possibly because those watchers are thought to be related to each other
64
+ # and when detaching one of them the other is also detached for some reasons.
65
+ # As a workaround, check if watchers are attached before detaching them.
66
+ @loop.watchers.each {|w| w.detach if w.attached? }
58
67
  @loop.stop
59
68
  @usock.close
60
69
  @thread.join
@@ -95,17 +104,20 @@ module Fluent
95
104
  # message Forward {
96
105
  # 1: string tag
97
106
  # 2: list<Entry> entries
107
+ # 3: object option (optional)
98
108
  # }
99
109
  #
100
110
  # message PackedForward {
101
111
  # 1: string tag
102
112
  # 2: raw entries # msgpack stream of Entry
113
+ # 3: object option (optional)
103
114
  # }
104
115
  #
105
116
  # message Message {
106
117
  # 1: string tag
107
118
  # 2: long? time
108
119
  # 3: object record
120
+ # 4: object option (optional)
109
121
  # }
110
122
  def on_message(msg, chunk_size, source)
111
123
  if msg.nil?
@@ -128,6 +140,7 @@ module Fluent
128
140
  # PackedForward
129
141
  es = MessagePackEventStream.new(entries)
130
142
  router.emit_stream(tag, es)
143
+ option = msg[2]
131
144
 
132
145
  elsif entries.class == Array
133
146
  # Forward
@@ -140,6 +153,7 @@ module Fluent
140
153
  es.add(time, record)
141
154
  }
142
155
  router.emit_stream(tag, es)
156
+ option = msg[2]
143
157
 
144
158
  else
145
159
  # Message
@@ -148,7 +162,11 @@ module Fluent
148
162
  time = msg[1]
149
163
  time = Engine.now if time == 0
150
164
  router.emit(tag, time, record)
165
+ option = msg[3]
151
166
  end
167
+
168
+ # return option for response
169
+ option
152
170
  end
153
171
 
154
172
  class Handler < Coolio::Socket
@@ -186,13 +204,16 @@ module Fluent
186
204
  first = data[0]
187
205
  if first == '{' || first == '['
188
206
  m = method(:on_read_json)
207
+ @serializer = :to_json.to_proc
189
208
  @y = Yajl::Parser.new
190
209
  @y.on_parse_complete = lambda { |obj|
191
- @on_message.call(obj, @chunk_counter, @source)
210
+ option = @on_message.call(obj, @chunk_counter, @source)
211
+ respond option if option
192
212
  @chunk_counter = 0
193
213
  }
194
214
  else
195
215
  m = method(:on_read_msgpack)
216
+ @serializer = :to_msgpack.to_proc
196
217
  @u = MessagePack::Unpacker.new
197
218
  end
198
219
 
@@ -214,7 +235,8 @@ module Fluent
214
235
  def on_read_msgpack(data)
215
236
  @chunk_counter += data.bytesize
216
237
  @u.feed_each(data) do |obj|
217
- @on_message.call(obj, @chunk_counter, @source)
238
+ option = @on_message.call(obj, @chunk_counter, @source)
239
+ respond option if option
218
240
  @chunk_counter = 0
219
241
  end
220
242
  rescue => e
@@ -223,8 +245,16 @@ module Fluent
223
245
  close
224
246
  end
225
247
 
248
+ def respond(option)
249
+ if option && option['chunk']
250
+ res = { 'ack' => option['chunk'] }
251
+ write @serializer.call(res)
252
+ @log.trace { "sent response to fluent socket" }
253
+ end
254
+ end
255
+
226
256
  def on_close
227
- @log.trace { "closed fluent socket object_id=#{self.object_id}" }
257
+ @log.trace { "closed socket" }
228
258
  end
229
259
  end
230
260
 
@@ -190,7 +190,6 @@ module Fluent
190
190
  @km = km
191
191
  @callback = callback
192
192
  @body_size_limit = body_size_limit
193
- @content_type = ""
194
193
  @next_close = false
195
194
  @format = format
196
195
  @log = log
@@ -235,6 +234,7 @@ module Fluent
235
234
  @keep_alive = false
236
235
  end
237
236
  @env = {}
237
+ @content_type = ""
238
238
  headers.each_pair {|k,v|
239
239
  @env["HTTP_#{k.gsub('-','_').upcase}"] = v
240
240
  case k
@@ -56,7 +56,7 @@ module Fluent
56
56
 
57
57
  if @time_key
58
58
  if @time_format
59
- tf = TimeFormatter.new(@time_format, @localtime)
59
+ tf = TimeFormatter.new(@time_format, @localtime, @timezone)
60
60
  @time_format_proc = tf.method(:format)
61
61
  else
62
62
  @time_format_proc = Proc.new { |time| time.to_s }
@@ -22,6 +22,7 @@ module Fluent
22
22
 
23
23
  def initialize
24
24
  super
25
+ require 'fluent/timezone'
25
26
  end
26
27
 
27
28
  config_param :command, :string
@@ -59,6 +60,7 @@ module Fluent
59
60
  config_param :time_format, :string, :default => nil
60
61
 
61
62
  config_param :localtime, :bool, :default => true
63
+ config_param :timezone, :string, :default => nil
62
64
  config_param :num_children, :integer, :default => 1
63
65
 
64
66
  # nil, 'none' or 0: no respawn, 'inf' or -1: infinite times, positive integer: try to respawn specified times only
@@ -96,13 +98,18 @@ module Fluent
96
98
  @localtime = false
97
99
  end
98
100
 
101
+ if conf['timezone']
102
+ @timezone = conf['timezone']
103
+ Fluent::Timezone.validate!(@timezone)
104
+ end
105
+
99
106
  if !@tag && !@out_tag_key
100
107
  raise ConfigError, "'tag' or 'out_tag_key' option is required on exec_filter output"
101
108
  end
102
109
 
103
110
  if @in_time_key
104
111
  if f = @in_time_format
105
- tf = TimeFormatter.new(f, @localtime)
112
+ tf = TimeFormatter.new(f, @localtime, @timezone)
106
113
  @time_format_proc = tf.method(:format)
107
114
  else
108
115
  @time_format_proc = Proc.new {|time| time.to_s }
@@ -15,11 +15,21 @@
15
15
  #
16
16
 
17
17
  module Fluent
18
+ class ForwardOutputError < StandardError
19
+ end
20
+
21
+ class ForwardOutputResponseError < ForwardOutputError
22
+ end
23
+
24
+ class ForwardOutputACKTimeoutError < ForwardOutputResponseError
25
+ end
26
+
18
27
  class ForwardOutput < ObjectBufferedOutput
19
28
  Plugin.register_output('forward', self)
20
29
 
21
30
  def initialize
22
31
  super
32
+ require "base64"
23
33
  require 'socket'
24
34
  require 'fileutils'
25
35
  require 'fluent/plugin/socket_util'
@@ -43,12 +53,22 @@ module Fluent
43
53
  config_param :expire_dns_cache, :time, :default => nil # 0 means disable cache
44
54
  config_param :phi_threshold, :integer, :default => 16
45
55
  config_param :phi_failure_detector, :bool, :default => true
56
+
57
+ # if any options added that requires extended forward api, fix @extend_internal_protocol
58
+
59
+ config_param :require_ack_response, :bool, :default => false # require in_forward to respond with ack
60
+ config_param :ack_response_timeout, :time, :default => 190 # 0 means do not wait for ack responses
61
+ # Linux default tcp_syn_retries is 5 (in many environment)
62
+ # 3 + 6 + 12 + 24 + 48 + 96 -> 189 (sec)
63
+
46
64
  attr_reader :nodes
47
65
 
48
66
  # backward compatibility
49
67
  config_param :port, :integer, :default => DEFAULT_LISTEN_PORT
50
68
  config_param :host, :string, :default => nil
51
69
 
70
+ attr_accessor :extend_internal_protocol
71
+
52
72
  def configure(conf)
53
73
  super
54
74
 
@@ -64,6 +84,12 @@ module Fluent
64
84
 
65
85
  recover_sample_size = @recover_wait / @heartbeat_interval
66
86
 
87
+ # add options here if any options addes which uses extended protocol
88
+ @extend_internal_protocol = if @require_ack_response
89
+ true
90
+ else
91
+ false
92
+ end
67
93
  conf.elements.each {|e|
68
94
  next if e.name != "server"
69
95
 
@@ -86,7 +112,7 @@ module Fluent
86
112
  node_conf = NodeConfig.new(name, host, port, weight, standby, failure,
87
113
  @phi_threshold, recover_sample_size, @expire_dns_cache, @phi_failure_detector)
88
114
  @nodes << Node.new(log, node_conf)
89
- log.info "adding forwarding server '#{name}'", :host=>host, :port=>port, :weight=>weight
115
+ log.info "adding forwarding server '#{name}'", :host=>host, :port=>port, :weight=>weight, :plugin_id=>plugin_id
90
116
  }
91
117
  end
92
118
 
@@ -200,8 +226,17 @@ module Fluent
200
226
  @weight_array = weight_array
201
227
  end
202
228
 
203
- # MessagePack FixArray length = 2
204
- FORWARD_HEADER = [0x92].pack('C')
229
+ # MessagePack FixArray length = 3 (if @extend_internal_protocol)
230
+ # = 2 (else)
231
+ FORWARD_HEADER = [0x92].pack('C').freeze
232
+ FORWARD_HEADER_EXT = [0x93].pack('C').freeze
233
+ def forward_header
234
+ if @extend_internal_protocol
235
+ FORWARD_HEADER_EXT
236
+ else
237
+ FORWARD_HEADER
238
+ end
239
+ end
205
240
 
206
241
  #FORWARD_TCP_HEARTBEAT_DATA = FORWARD_HEADER + ''.to_msgpack + [].to_msgpack
207
242
  def send_heartbeat_tcp(node)
@@ -229,7 +264,7 @@ module Fluent
229
264
  sock.setsockopt(Socket::SOL_SOCKET, Socket::SO_SNDTIMEO, opt)
230
265
 
231
266
  # beginArray(2)
232
- sock.write FORWARD_HEADER
267
+ sock.write forward_header
233
268
 
234
269
  # writeRaw(tag)
235
270
  sock.write tag.to_msgpack # tag
@@ -250,7 +285,46 @@ module Fluent
250
285
  # writeRawBody(packed_es)
251
286
  chunk.write_to(sock)
252
287
 
288
+ if @extend_internal_protocol
289
+ option = {}
290
+ option['chunk'] = Base64.encode64(chunk.unique_id) if @require_ack_response
291
+ sock.write option.to_msgpack
292
+
293
+ if @require_ack_response && @ack_response_timeout > 0
294
+ # Waiting for a response here results in a decrease of throughput because a chunk queue is locked.
295
+ # To avoid a decrease of troughput, it is necessary to prepare a list of chunks that wait for responses
296
+ # and process them asynchronously.
297
+ if IO.select([sock], nil, nil, @ack_response_timeout)
298
+ raw_data = sock.recv(1024)
299
+
300
+ # When connection is closed by remote host, socket is ready to read and #recv returns an empty string that means EOF.
301
+ # In this case, the node is available and successfully close connection without sending responses.
302
+ # ForwardInput is not expected to do so, but some alternatives may do so.
303
+ # Therefore do not send the chunk again.
304
+ unless raw_data.empty?
305
+ # Serialization type of the response is same as sent data.
306
+ res = MessagePack.unpack(raw_data)
307
+
308
+ if res['ack'] != option['chunk']
309
+ # Some errors may have occured when ack and chunk id is different, so send the chunk again.
310
+ raise ForwardOutputResponseError, "ack in response and chunk id in sent data are different"
311
+ end
312
+ end
313
+
314
+ else
315
+ # IO.select returns nil on timeout.
316
+ # There are 2 types of cases when no response has been received:
317
+ # (1) the node does not support sending responses
318
+ # (2) the node does support sending response but responses have not arrived for some reasons.
319
+ @log.warn "no response from #{node.host}:#{node.port}. regard it as unavailable."
320
+ node.disable!
321
+ raise ForwardOutputACKTimeoutError, "node #{node.host}:#{node.port} does not return ACK"
322
+ end
323
+ end
324
+ end
325
+
253
326
  node.heartbeat(false)
327
+ return res # for test
254
328
  ensure
255
329
  sock.close
256
330
  end
@@ -354,6 +428,10 @@ module Fluent
354
428
  @available
355
429
  end
356
430
 
431
+ def disable!
432
+ @available = false
433
+ end
434
+
357
435
  def standby?
358
436
  @conf.standby
359
437
  end