fluentd 0.14.8 → 0.14.9

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 (95) hide show
  1. checksums.yaml +4 -4
  2. data/.gitignore +2 -0
  3. data/CONTRIBUTING.md +6 -1
  4. data/ChangeLog +38 -0
  5. data/Rakefile +21 -0
  6. data/example/out_exec_filter.conf +42 -0
  7. data/lib/fluent/agent.rb +2 -2
  8. data/lib/fluent/command/binlog_reader.rb +1 -1
  9. data/lib/fluent/command/cat.rb +5 -2
  10. data/lib/fluent/compat/output.rb +7 -8
  11. data/lib/fluent/compat/parser.rb +139 -11
  12. data/lib/fluent/config/configure_proxy.rb +2 -11
  13. data/lib/fluent/config/section.rb +7 -0
  14. data/lib/fluent/configurable.rb +1 -3
  15. data/lib/fluent/log.rb +1 -1
  16. data/lib/fluent/plugin/base.rb +17 -0
  17. data/lib/fluent/plugin/filter_parser.rb +108 -0
  18. data/lib/fluent/plugin/filter_record_transformer.rb +4 -7
  19. data/lib/fluent/plugin/filter_stdout.rb +1 -1
  20. data/lib/fluent/plugin/formatter.rb +5 -0
  21. data/lib/fluent/plugin/formatter_msgpack.rb +4 -0
  22. data/lib/fluent/plugin/formatter_stdout.rb +3 -2
  23. data/lib/fluent/plugin/formatter_tsv.rb +34 -0
  24. data/lib/fluent/plugin/in_exec.rb +48 -93
  25. data/lib/fluent/plugin/in_forward.rb +25 -105
  26. data/lib/fluent/plugin/in_http.rb +68 -65
  27. data/lib/fluent/plugin/in_syslog.rb +29 -51
  28. data/lib/fluent/plugin/multi_output.rb +1 -3
  29. data/lib/fluent/plugin/out_exec.rb +58 -71
  30. data/lib/fluent/plugin/out_exec_filter.rb +199 -279
  31. data/lib/fluent/plugin/out_file.rb +155 -80
  32. data/lib/fluent/plugin/out_forward.rb +44 -47
  33. data/lib/fluent/plugin/out_stdout.rb +6 -21
  34. data/lib/fluent/plugin/output.rb +23 -17
  35. data/lib/fluent/plugin/parser.rb +121 -61
  36. data/lib/fluent/plugin/parser_csv.rb +9 -3
  37. data/lib/fluent/plugin/parser_json.rb +37 -35
  38. data/lib/fluent/plugin/parser_ltsv.rb +11 -19
  39. data/lib/fluent/plugin/parser_msgpack.rb +50 -0
  40. data/lib/fluent/plugin/parser_regexp.rb +15 -42
  41. data/lib/fluent/plugin/parser_tsv.rb +8 -3
  42. data/lib/fluent/plugin_helper.rb +8 -1
  43. data/lib/fluent/plugin_helper/child_process.rb +139 -73
  44. data/lib/fluent/plugin_helper/compat_parameters.rb +93 -4
  45. data/lib/fluent/plugin_helper/event_emitter.rb +14 -1
  46. data/lib/fluent/plugin_helper/extract.rb +16 -4
  47. data/lib/fluent/plugin_helper/formatter.rb +9 -11
  48. data/lib/fluent/plugin_helper/inject.rb +4 -0
  49. data/lib/fluent/plugin_helper/parser.rb +3 -3
  50. data/lib/fluent/root_agent.rb +1 -1
  51. data/lib/fluent/test/driver/base.rb +51 -37
  52. data/lib/fluent/test/driver/base_owner.rb +18 -8
  53. data/lib/fluent/test/driver/multi_output.rb +2 -1
  54. data/lib/fluent/test/driver/output.rb +29 -6
  55. data/lib/fluent/test/helpers.rb +3 -1
  56. data/lib/fluent/test/log.rb +4 -0
  57. data/lib/fluent/test/startup_shutdown.rb +13 -0
  58. data/lib/fluent/time.rb +14 -8
  59. data/lib/fluent/version.rb +1 -1
  60. data/test/command/test_binlog_reader.rb +5 -1
  61. data/test/config/test_configurable.rb +173 -0
  62. data/test/config/test_configure_proxy.rb +0 -43
  63. data/test/plugin/test_base.rb +16 -0
  64. data/test/plugin/test_filter_parser.rb +665 -0
  65. data/test/plugin/test_filter_record_transformer.rb +11 -3
  66. data/test/plugin/test_filter_stdout.rb +18 -27
  67. data/test/plugin/test_in_dummy.rb +1 -1
  68. data/test/plugin/test_in_exec.rb +206 -94
  69. data/test/plugin/test_in_forward.rb +310 -327
  70. data/test/plugin/test_in_http.rb +310 -186
  71. data/test/plugin/test_out_exec.rb +223 -68
  72. data/test/plugin/test_out_exec_filter.rb +520 -169
  73. data/test/plugin/test_out_file.rb +620 -177
  74. data/test/plugin/test_out_forward.rb +110 -132
  75. data/test/plugin/test_out_null.rb +1 -1
  76. data/test/plugin/test_out_secondary_file.rb +4 -2
  77. data/test/plugin/test_out_stdout.rb +14 -35
  78. data/test/plugin/test_parser.rb +359 -0
  79. data/test/plugin/test_parser_csv.rb +1 -2
  80. data/test/plugin/test_parser_json.rb +3 -4
  81. data/test/plugin/test_parser_labeled_tsv.rb +1 -2
  82. data/test/plugin/test_parser_none.rb +1 -2
  83. data/test/plugin/test_parser_regexp.rb +8 -4
  84. data/test/plugin/test_parser_tsv.rb +4 -3
  85. data/test/plugin_helper/test_child_process.rb +184 -0
  86. data/test/plugin_helper/test_compat_parameters.rb +88 -1
  87. data/test/plugin_helper/test_extract.rb +0 -1
  88. data/test/plugin_helper/test_formatter.rb +5 -2
  89. data/test/plugin_helper/test_parser.rb +6 -5
  90. data/test/test_output.rb +24 -2
  91. data/test/test_plugin_classes.rb +20 -0
  92. data/test/test_root_agent.rb +139 -0
  93. data/test/test_test_drivers.rb +132 -0
  94. metadata +12 -4
  95. data/test/plugin/test_parser_base.rb +0 -32
@@ -25,8 +25,9 @@ module Fluent::Plugin
25
25
  class SyslogInput < Input
26
26
  Fluent::Plugin.register_input('syslog', self)
27
27
 
28
- helpers :parser, :event_loop
28
+ helpers :parser, :compat_parameters, :event_loop
29
29
 
30
+ DEFAULT_PARSER = 'syslog'
30
31
  SYSLOG_REGEXP = /^\<([0-9]+)\>(.*)/
31
32
 
32
33
  FACILITY_MAP = {
@@ -79,16 +80,7 @@ module Fluent::Plugin
79
80
  desc 'The prefix of the tag. The tag itself is generated by the tag prefix, facility level, and priority.'
80
81
  config_param :tag, :string
81
82
  desc 'The transport protocol used to receive logs.(udp, tcp)'
82
- config_param :protocol_type, default: :udp do |val|
83
- case val.downcase
84
- when 'tcp'
85
- :tcp
86
- when 'udp'
87
- :udp
88
- else
89
- raise Fluent::ConfigError, "syslog input protocol type should be 'tcp' or 'udp'"
90
- end
91
- end
83
+ config_param :protocol_type, :enum, list: [:tcp, :udp], default: :udp
92
84
  desc 'If true, add source host to event record.'
93
85
  config_param :include_source_host, :bool, default: false
94
86
  desc 'Specify key of source host when include_source_host is true.'
@@ -96,31 +88,28 @@ module Fluent::Plugin
96
88
  config_param :blocking_timeout, :time, default: 0.5
97
89
  config_param :message_length_limit, :size, default: 2048
98
90
 
91
+ config_section :parse do
92
+ config_set_default :@type, DEFAULT_PARSER
93
+ config_param :with_priority, :bool, default: true
94
+ end
95
+
99
96
  def configure(conf)
97
+ compat_parameters_convert(conf, :parser)
98
+
100
99
  super
101
100
 
102
101
  @use_default = false
103
102
 
104
- if conf.has_key?('format')
105
- @parser = parser_create(usage: 'syslog_input', type: conf['format'], conf: conf)
106
- else
107
- conf['with_priority'] = true
108
- @parser = parser_create(usage: 'syslog_input', type: 'syslog', conf: conf)
109
- @use_default = true
110
- end
103
+ @parser = parser_create
104
+ @parser_parse_priority = @parser.respond_to?(:with_priority) && @parser.with_priority
105
+
111
106
  @_event_loop_run_timeout = @blocking_timeout
112
107
  end
113
108
 
114
109
  def start
115
110
  super
116
111
 
117
- callback = if @use_default
118
- method(:receive_data)
119
- else
120
- method(:receive_data_parser)
121
- end
122
-
123
- @handler = listen(callback)
112
+ @handler = listen(method(:message_handler))
124
113
  event_loop_attach(@handler)
125
114
  end
126
115
 
@@ -132,42 +121,31 @@ module Fluent::Plugin
132
121
 
133
122
  private
134
123
 
135
- def receive_data_parser(data, addr)
136
- m = SYSLOG_REGEXP.match(data)
137
- unless m
138
- log.warn "invalid syslog message: #{data.dump}"
139
- return
140
- end
141
- pri = m[1].to_i
142
- text = m[2]
143
-
144
- @parser.parse(text) { |time, record|
145
- unless time && record
146
- log.warn "pattern not match: #{text.inspect}"
124
+ def message_handler(data, addr)
125
+ pri = nil
126
+ text = data
127
+ unless @parser_parse_priority
128
+ m = SYSLOG_REGEXP.match(data)
129
+ unless m
130
+ log.warn "invalid syslog message: #{data.dump}"
147
131
  return
148
132
  end
133
+ pri = m[1].to_i
134
+ text = m[2]
135
+ end
149
136
 
150
- record[@source_host_key] = addr[2] if @include_source_host
151
- emit(pri, time, record)
152
- }
153
- rescue => e
154
- log.error data.dump, error: e.to_s
155
- log.error_backtrace
156
- end
157
-
158
- def receive_data(data, addr)
159
- @parser.parse(data) { |time, record|
137
+ @parser.parse(text) do |time, record|
160
138
  unless time && record
161
- log.warn "invalid syslog message", data: data
139
+ log.warn "failed to parse message", data: data
162
140
  return
163
141
  end
164
142
 
165
- pri = record.delete('pri')
143
+ pri ||= record.delete('pri')
166
144
  record[@source_host_key] = addr[2] if @include_source_host
167
145
  emit(pri, time, record)
168
- }
146
+ end
169
147
  rescue => e
170
- log.error data.dump, error: e.to_s
148
+ log.error "invalid input", data: data, error: e
171
149
  log.error_backtrace
172
150
  end
173
151
 
@@ -69,9 +69,7 @@ module Fluent
69
69
  log.debug "adding store", type: type
70
70
 
71
71
  output = Fluent::Plugin.new_output(type)
72
- if output.has_router?
73
- output.router = router
74
- end
72
+ output.context_router = self.context_router
75
73
  output.configure(store_conf)
76
74
  @outputs << output
77
75
  end
@@ -18,97 +18,84 @@ require 'tempfile'
18
18
 
19
19
  require 'fluent/plugin/output'
20
20
  require 'fluent/config/error'
21
- require 'fluent/plugin/exec_util'
22
- require 'fluent/mixin' # for TimeFormatter
23
21
 
24
22
  module Fluent::Plugin
25
23
  class ExecOutput < Output
26
24
  Fluent::Plugin.register_output('exec', self)
27
25
 
28
- helpers :compat_parameters
26
+ helpers :inject, :formatter, :compat_parameters, :child_process
29
27
 
30
- desc 'The command (program) to execute. The exec plugin passes the path of a TSV file as the last argumen'
28
+ desc 'The command (program) to execute. The exec plugin passes the path of a TSV file as the last argument'
31
29
  config_param :command, :string
32
- desc 'Specify the comma-separated keys when using the tsv format.'
33
- config_param :keys, default: [] do |val|
34
- val.split(',')
30
+
31
+ config_param :command_timeout, :time, default: 270 # 4min 30sec
32
+
33
+ config_section :format do
34
+ config_set_default :@type, 'tsv'
35
35
  end
36
- desc 'The name of the key to use as the event tag. This replaces the value in the event record.'
37
- config_param :tag_key, :string, default: nil
38
- desc 'The name of the key to use as the event time. This replaces the the value in the event record.'
39
- config_param :time_key, :string, default: nil
40
- desc 'The format for event time used when the time_key parameter is specified. The default is UNIX time (integer).'
41
- config_param :time_format, :string, default: nil
42
- desc "The format used to map the incoming events to the program input. (#{Fluent::ExecUtil::SUPPORTED_FORMAT.keys.join(',')})"
43
- config_param :format, default: :tsv, skip_accessor: true do |val|
44
- f = Fluent::ExecUtil::SUPPORTED_FORMAT[val]
45
- raise Fluent::ConfigError, "Unsupported format '#{val}'" unless f
46
- f
36
+
37
+ config_section :inject do
38
+ config_set_default :time_type, :string
39
+ config_set_default :localtime, false
47
40
  end
48
- config_param :localtime, :bool, default: false
49
- config_param :timezone, :string, default: nil
50
41
 
51
- def compat_parameters_default_chunk_key
52
- 'time'
42
+ config_section :buffer do
43
+ config_set_default :delayed_commit_timeout, 300 # 5 min
53
44
  end
54
45
 
55
- def configure(conf)
56
- compat_parameters_convert(conf, :buffer, default_chunk_key: 'time')
46
+ attr_reader :formatter # for tests
57
47
 
48
+ def configure(conf)
49
+ compat_parameters_convert(conf, :inject, :formatter, :buffer, default_chunk_key: 'time')
58
50
  super
59
-
60
- @formatter = case @format
61
- when :tsv
62
- if @keys.empty?
63
- raise Fluent::ConfigError, "keys option is required on exec output for tsv format"
64
- end
65
- Fluent::ExecUtil::TSVFormatter.new(@keys)
66
- when :json
67
- Fluent::ExecUtil::JSONFormatter.new
68
- when :msgpack
69
- Fluent::ExecUtil::MessagePackFormatter.new
70
- end
71
-
72
- if @time_key
73
- if @time_format
74
- tf = Fluent::TimeFormatter.new(@time_format, @localtime, @timezone)
75
- @time_format_proc = tf.method(:format)
76
- else
77
- @time_format_proc = Proc.new { |time| time.to_s }
78
- end
79
- end
51
+ @formatter = formatter_create
80
52
  end
81
53
 
82
- def format(tag, time, record)
83
- out = ''
84
- if @time_key
85
- record[@time_key] = @time_format_proc.call(time)
86
- end
87
- if @tag_key
88
- record[@tag_key] = tag
89
- end
90
- @formatter.call(record, out)
91
- out
92
- end
54
+ NEWLINE = "\n"
93
55
 
94
- def write(chunk)
95
- if chunk.respond_to?(:path)
96
- prog = "#{@command} #{chunk.path}"
56
+ def format(tag, time, record)
57
+ record = inject_values_to_record(tag, time, record)
58
+ if @formatter.formatter_type == :text_per_line
59
+ @formatter.format(tag, time, record).chomp + NEWLINE
97
60
  else
98
- tmpfile = Tempfile.new("fluent-plugin-exec-")
99
- tmpfile.binmode
100
- chunk.write_to(tmpfile)
101
- tmpfile.close
102
- prog = "#{@command} #{tmpfile.path}"
61
+ @formatter.format(tag, time, record)
103
62
  end
63
+ end
104
64
 
105
- system(prog)
106
- ecode = $?.to_i
107
- tmpfile.delete if tmpfile
108
-
109
- if ecode != 0
110
- raise "command returns #{ecode}: #{prog}"
111
- end
65
+ def try_write(chunk)
66
+ tmpfile = nil
67
+ prog = if chunk.respond_to?(:path)
68
+ "#{@command} #{chunk.path}"
69
+ else
70
+ tmpfile = Tempfile.new("fluent-plugin-out-exec-")
71
+ tmpfile.binmode
72
+ chunk.write_to(tmpfile)
73
+ tmpfile.close
74
+ "#{@command} #{tmpfile.path}"
75
+ end
76
+ chunk_id = chunk.unique_id
77
+ callback = ->(status){
78
+ begin
79
+ if tmpfile
80
+ tmpfile.delete rescue nil
81
+ end
82
+ if status && status.success?
83
+ commit_write(chunk_id)
84
+ elsif status
85
+ # #rollback_write will be done automatically if it isn't called at here.
86
+ # But it's after command_timeout, and this timeout should be longer than users expectation.
87
+ # So here, this plugin calls it explicitly.
88
+ rollback_write(chunk_id)
89
+ log.warn "command exits with error code", prog: prog, status: status.exitstatus, signal: status.termsig
90
+ else
91
+ rollback_write(chunk_id)
92
+ log.warn "command unexpectedly exits without exit status", prog: prog
93
+ end
94
+ rescue => e
95
+ log.error "unexpected error in child process callback", error: e
96
+ end
97
+ }
98
+ child_process_execute(:out_exec_process, prog, stderr: :connect, immediate: true, parallel: true, mode: [], wait_timeout: @command_timeout, on_exit_callback: callback)
112
99
  end
113
100
  end
114
101
  end
@@ -13,148 +13,142 @@
13
13
  # See the License for the specific language governing permissions and
14
14
  # limitations under the License.
15
15
  #
16
-
17
- require 'yajl'
18
-
19
- require 'fluent/output'
16
+ require 'fluent/plugin/output'
20
17
  require 'fluent/env'
21
- require 'fluent/time'
22
- require 'fluent/timezone'
23
- require 'fluent/plugin/exec_util'
24
18
  require 'fluent/config/error'
25
19
 
26
- module Fluent
27
- class ExecFilterOutput < BufferedOutput
28
- Plugin.register_output('exec_filter', self)
20
+ require 'yajl'
29
21
 
30
- def initialize
31
- super
32
- require 'fluent/timezone'
33
- end
22
+ module Fluent::Plugin
23
+ class ExecFilterOutput < Output
24
+ Fluent::Plugin.register_output('exec_filter', self)
25
+
26
+ helpers :compat_parameters, :inject, :formatter, :parser, :extract, :child_process, :event_emitter
34
27
 
35
28
  desc 'The command (program) to execute.'
36
29
  config_param :command, :string
37
30
 
38
- config_param :remove_prefix, :string, default: nil
39
- config_param :add_prefix, :string, default: nil
31
+ config_param :remove_prefix, :string, default: nil, deprecated: "use @label instead for event routing"
32
+ config_param :add_prefix, :string, default: nil, deprecated: "use @label instead for event routing"
33
+
34
+ config_section :inject do
35
+ config_set_default :time_type, :unixtime
36
+ end
40
37
 
41
- desc "The format used to map the incoming event to the program input.(#{Fluent::ExecUtil::SUPPORTED_FORMAT.keys.join(',')})"
42
- config_param :in_format, default: :tsv do |val|
43
- f = Fluent::ExecUtil::SUPPORTED_FORMAT[val]
44
- raise ConfigError, "Unsupported in_format '#{val}'" unless f
45
- f
38
+ config_section :format do
39
+ config_set_default :@type, 'tsv'
40
+ config_set_default :localtime, true
46
41
  end
47
- desc 'Specify comma-separated values for tsv format.'
48
- config_param :in_keys, default: [] do |val|
49
- val.split(',')
42
+
43
+ config_section :parse do
44
+ config_set_default :@type, 'tsv'
45
+ config_set_default :time_key, nil
46
+ config_set_default :time_format, nil
47
+ config_set_default :localtime, true
48
+ config_set_default :estimate_current_event, false
50
49
  end
51
- desc 'The name of the key to use as the event tag.'
52
- config_param :in_tag_key, default: nil
53
- desc 'The name of the key to use as the event time.'
54
- config_param :in_time_key, default: nil
55
- desc 'The format for event time used when the in_time_key parameter is specified.(Defauls is UNIX time)'
56
- config_param :in_time_format, default: nil
57
-
58
- desc "The format used to process the program output.(#{Fluent::ExecUtil::SUPPORTED_FORMAT.keys.join(',')})"
59
- config_param :out_format, default: :tsv do |val|
60
- f = Fluent::ExecUtil::SUPPORTED_FORMAT[val]
61
- raise ConfigError, "Unsupported out_format '#{val}'" unless f
62
- f
50
+
51
+ config_section :extract do
52
+ config_set_default :time_type, :float
63
53
  end
64
- desc 'Specify comma-separated values for tsv format.'
65
- config_param :out_keys, default: [] do |val| # for tsv format
66
- val.split(',')
54
+
55
+ config_section :buffer do
56
+ config_set_default :flush_mode, :interval
57
+ config_set_default :flush_interval, 1
67
58
  end
68
- desc 'The name of the key to use as the event tag.'
69
- config_param :out_tag_key, default: nil
70
- desc 'The name of the key to use as the event time.'
71
- config_param :out_time_key, default: nil
72
- desc 'The format for event time used when the in_time_key parameter is specified.(Defauls is UNIX time)'
73
- config_param :out_time_format, default: nil
74
59
 
75
60
  config_param :tag, :string, default: nil
76
61
 
77
- config_param :time_key, :string, default: nil
78
- config_param :time_format, :string, default: nil
62
+ config_param :tag_key, :string, default: nil, deprecated: "use 'tag_key' in <inject>/<extract> instead"
63
+ config_param :time_key, :string, default: nil, deprecated: "use 'time_key' in <inject>/<extract> instead"
64
+ config_param :time_format, :string, default: nil, deprecated: "use 'time_format' in <inject>/<extract> instead"
65
+
66
+ desc 'The default block size to read if parser requires partial read.'
67
+ config_param :read_block_size, :size, default: 10240 # 10k
79
68
 
80
- desc 'If true, use localtime with in_time_format.'
81
- config_param :localtime, :bool, default: true
82
- desc 'If true, use timezone with in_time_format.'
83
- config_param :timezone, :string, default: nil
84
69
  desc 'The number of spawned process for command.'
85
70
  config_param :num_children, :integer, default: 1
86
71
 
87
- desc 'Respawn command when command exit.'
72
+ desc 'Respawn command when command exit. ["none", "inf" or positive integer for times to respawn (defaut: none)]'
88
73
  # nil, 'none' or 0: no respawn, 'inf' or -1: infinite times, positive integer: try to respawn specified times only
89
74
  config_param :child_respawn, :string, default: nil
90
75
 
91
76
  # 0: output logs for all of messages to emit
92
77
  config_param :suppress_error_log_interval, :time, default: 0
93
78
 
94
- config_set_default :flush_interval, 1
95
-
96
- def configure(conf)
97
- if tag_key = conf['tag_key']
98
- # TODO obsoleted?
99
- @in_tag_key = tag_key
100
- @out_tag_key = tag_key
101
- end
102
-
103
- if time_key = conf['time_key']
104
- # TODO obsoleted?
105
- @in_time_key = time_key
106
- @out_time_key = time_key
79
+ attr_reader :formatter, :parser # for tests
80
+
81
+ KEYS_FOR_IN_AND_OUT = {
82
+ 'tag_key' => ['in_tag_key', 'out_tag_key'],
83
+ 'time_key' => ['in_time_key', 'out_time_key'],
84
+ 'time_format' => ['in_time_format', 'out_time_format'],
85
+ }
86
+ COMPAT_INJECT_PARAMS = {
87
+ 'in_tag_key' => 'tag_key',
88
+ 'in_time_key' => 'time_key',
89
+ 'in_time_format' => 'time_format',
90
+ }
91
+ COMPAT_FORMAT_PARAMS = {
92
+ 'in_format' => '@type',
93
+ 'in_keys' => 'keys',
94
+ }
95
+ COMPAT_PARSE_PARAMS = {
96
+ 'out_format' => '@type',
97
+ 'out_keys' => 'keys',
98
+ }
99
+ COMPAT_EXTRACT_PARAMS = {
100
+ 'out_tag_key' => 'tag_key',
101
+ 'out_time_key' => 'time_key',
102
+ 'out_time_format' => 'time_format',
103
+ }
104
+
105
+ def exec_filter_compat_parameters_copy_to_subsection!(conf, subsection_name, params)
106
+ return unless conf.elements(subsection_name).empty?
107
+ return unless params.keys.any?{|k| conf.has_key?(k) }
108
+ hash = {}
109
+ params.each_pair do |compat, current|
110
+ hash[current] = conf[compat] if conf.has_key?(compat)
107
111
  end
112
+ conf.elements << Fluent::Config::Element.new(subsection_name, '', hash, [])
113
+ end
108
114
 
109
- if time_format = conf['time_format']
110
- # TODO obsoleted?
111
- @in_time_format = time_format
112
- @out_time_format = time_format
115
+ def exec_filter_compat_parameters_convert!(conf)
116
+ KEYS_FOR_IN_AND_OUT.each_pair do |inout, keys|
117
+ if conf.has_key?(inout)
118
+ keys.each do |k|
119
+ conf[k] = conf[inout]
120
+ end
121
+ end
113
122
  end
123
+ exec_filter_compat_parameters_copy_to_subsection!(conf, 'inject', COMPAT_INJECT_PARAMS)
124
+ exec_filter_compat_parameters_copy_to_subsection!(conf, 'format', COMPAT_FORMAT_PARAMS)
125
+ exec_filter_compat_parameters_copy_to_subsection!(conf, 'parse', COMPAT_PARSE_PARAMS)
126
+ exec_filter_compat_parameters_copy_to_subsection!(conf, 'extract', COMPAT_EXTRACT_PARAMS)
127
+ end
114
128
 
115
- super
129
+ def configure(conf)
130
+ exec_filter_compat_parameters_convert!(conf)
131
+ compat_parameters_convert(conf, :buffer)
116
132
 
117
- if conf['localtime']
118
- @localtime = true
119
- elsif conf['utc']
120
- @localtime = false
133
+ if inject_section = conf.elements('inject').first
134
+ if inject_section.has_key?('time_format')
135
+ inject_section['time_type'] ||= 'string'
136
+ end
121
137
  end
122
-
123
- if conf['timezone']
124
- @timezone = conf['timezone']
125
- Fluent::Timezone.validate!(@timezone)
138
+ if extract_section = conf.elements('extract').first
139
+ if extract_section.has_key?('time_format')
140
+ extract_section['time_type'] ||= 'string'
141
+ end
126
142
  end
127
143
 
128
- if !@tag && !@out_tag_key
129
- raise ConfigError, "'tag' or 'out_tag_key' option is required on exec_filter output"
130
- end
144
+ super
131
145
 
132
- if @in_time_key
133
- if f = @in_time_format
134
- tf = TimeFormatter.new(f, @localtime, @timezone)
135
- @time_format_proc = tf.method(:format)
136
- else
137
- @time_format_proc = Proc.new {|time| time.to_s }
138
- end
139
- elsif @in_time_format
140
- log.warn "in_time_format effects nothing when in_time_key is not specified: #{conf}"
146
+ if !@tag && (!@extract_config || !@extract_config.tag_key)
147
+ raise Fluent::ConfigError, "'tag' or '<extract> tag_key </extract>' option is required on exec_filter output"
141
148
  end
142
149
 
143
- if @out_time_key
144
- if f = @out_time_format
145
- @time_parse_proc =
146
- begin
147
- strptime = Strptime.new(f)
148
- Proc.new { |str| Fluent::EventTime.from_time(strptime.exec(str)) }
149
- rescue
150
- Proc.new {|str| Fluent::EventTime.from_time(Time.strptime(str, f)) }
151
- end
152
- else
153
- @time_parse_proc = Proc.new {|str| Fluent::EventTime.from_time(Time.at(str.to_f)) }
154
- end
155
- elsif @out_time_format
156
- log.warn "out_time_format effects nothing when out_time_key is not specified: #{conf}"
157
- end
150
+ @formatter = formatter_create
151
+ @parser = parser_create
158
152
 
159
153
  if @remove_prefix
160
154
  @removed_prefix_string = @remove_prefix + '.'
@@ -164,30 +158,6 @@ module Fluent
164
158
  @added_prefix_string = @add_prefix + '.'
165
159
  end
166
160
 
167
- case @in_format
168
- when :tsv
169
- if @in_keys.empty?
170
- raise ConfigError, "in_keys option is required on exec_filter output for tsv in_format"
171
- end
172
- @formatter = Fluent::ExecUtil::TSVFormatter.new(@in_keys)
173
- when :json
174
- @formatter = Fluent::ExecUtil::JSONFormatter.new
175
- when :msgpack
176
- @formatter = Fluent::ExecUtil::MessagePackFormatter.new
177
- end
178
-
179
- case @out_format
180
- when :tsv
181
- if @out_keys.empty?
182
- raise ConfigError, "out_keys option is required on exec_filter output for tsv in_format"
183
- end
184
- @parser = Fluent::ExecUtil::TSVParser.new(@out_keys, method(:on_message))
185
- when :json
186
- @parser = Fluent::ExecUtil::JSONParser.new(method(:on_message))
187
- when :msgpack
188
- @parser = Fluent::ExecUtil::MessagePackParser.new(method(:on_message))
189
- end
190
-
191
161
  @respawns = if @child_respawn.nil? or @child_respawn == 'none' or @child_respawn == '0'
192
162
  0
193
163
  elsif @child_respawn == 'inf' or @child_respawn == '-1'
@@ -202,192 +172,142 @@ module Fluent
202
172
  @next_log_time = Time.now.to_i
203
173
  end
204
174
 
175
+ ExecutedProcess = Struct.new(:mutex, :pid, :respawns, :readio, :writeio)
176
+
205
177
  def start
206
178
  super
207
179
 
180
+ @children_mutex = Mutex.new
208
181
  @children = []
209
182
  @rr = 0
210
- begin
211
- @num_children.times do
212
- c = ChildProcess.new(@parser, @respawns, log)
213
- c.start(@command)
214
- @children << c
215
- end
216
- rescue
217
- shutdown
218
- raise
219
- end
220
- end
221
183
 
222
- def before_shutdown
223
- log.debug "out_exec_filter#before_shutdown called"
224
- @children.each {|c|
225
- c.finished = true
226
- }
227
- sleep 0.5 # TODO wait time before killing child process
228
-
229
- super
230
- end
231
-
232
- def shutdown
233
- @children.reject! {|c|
234
- c.shutdown
235
- true
184
+ exit_callback = ->(status){
185
+ c = @children.select{|child| child.pid == status.pid }.first
186
+ if c
187
+ unless self.stopped?
188
+ log.warn "child process exits with error code", code: status.to_i, status: status.exitstatus, signal: status.termsig
189
+ end
190
+ c.mutex.synchronize do
191
+ (c.writeio && c.writeio.close) rescue nil
192
+ (c.readio && c.readio.close) rescue nil
193
+ c.pid = c.readio = c.writeio = nil
194
+ end
195
+ end
236
196
  }
237
-
238
- super
239
- end
240
-
241
- def format_stream(tag, es)
242
- if @remove_prefix
243
- if (tag[0, @removed_length] == @removed_prefix_string and tag.length > @removed_length) or tag == @removed_prefix
244
- tag = tag[@removed_length..-1] || ''
197
+ child_process_callback = ->(index, readio, writeio){
198
+ pid = child_process_id
199
+ c = @children[index]
200
+ writeio.sync = true
201
+ c.mutex.synchronize do
202
+ c.pid = pid
203
+ c.respawns = @respawns
204
+ c.readio = readio
205
+ c.writeio = writeio
245
206
  end
246
- end
247
207
 
248
- out = ''
249
-
250
- es.each {|time,record|
251
- if @in_time_key
252
- record[@in_time_key] = @time_format_proc.call(time)
253
- end
254
- if @in_tag_key
255
- record[@in_tag_key] = tag
208
+ run(readio)
209
+ }
210
+ execute_child_process = ->(index){
211
+ child_process_execute("out_exec_filter_child#{index}".to_sym, @command, on_exit_callback: exit_callback) do |readio, writeio|
212
+ child_process_callback.call(index, readio, writeio)
256
213
  end
257
- @formatter.call(record, out)
258
214
  }
259
215
 
260
- out
261
- end
262
-
263
- def write(chunk)
264
- r = @rr = (@rr + 1) % @children.length
265
- @children[r].write chunk
266
- end
267
-
268
- class ChildProcess
269
- attr_accessor :finished
270
-
271
- def initialize(parser, respawns=0, log = $log)
272
- @pid = nil
273
- @thread = nil
274
- @parser = parser
275
- @respawns = respawns
276
- @mutex = Mutex.new
277
- @finished = nil
278
- @log = log
279
- end
280
-
281
- def start(command)
282
- @command = command
283
- @mutex.synchronize do
284
- @io = IO.popen(command, "r+")
285
- @pid = @io.pid
286
- @io.sync = true
287
- @thread = Thread.new(&method(:run))
216
+ @children_mutex.synchronize do
217
+ @num_children.times do |i|
218
+ @children << ExecutedProcess.new(Mutex.new, nil, 0, nil, nil)
219
+ execute_child_process.call(i)
288
220
  end
289
- @finished = false
290
221
  end
291
222
 
292
- def kill_child(join_wait)
293
- begin
294
- signal = Fluent.windows? ? :KILL : :TERM
295
- Process.kill(signal, @pid)
296
- rescue #Errno::ECHILD, Errno::ESRCH, Errno::EPERM
297
- # Errno::ESRCH 'No such process', ignore
298
- # child process killed by signal chained from fluentd process
299
- end
300
- if @thread.join(join_wait)
301
- # @thread successfully shutdown
302
- return
303
- end
304
- begin
305
- Process.kill(:KILL, @pid)
306
- rescue #Errno::ECHILD, Errno::ESRCH, Errno::EPERM
307
- # ignore if successfully killed by :TERM
223
+ if @respawns != 0
224
+ thread_create(:out_exec_filter_respawn_monitor) do
225
+ while thread_current_running?
226
+ @children.each_with_index do |c, i|
227
+ if c.mutex && c.mutex.synchronize{ c.pid.nil? && c.respawns != 0 }
228
+ respawns = c.mutex.synchronize do
229
+ c.respawns -= 1 if c.respawns > 0
230
+ c.respawns
231
+ end
232
+ log.info "respawning child process", num: i, respawn_counter: respawns
233
+ execute_child_process.call(i)
234
+ end
235
+ end
236
+ sleep 0.2
237
+ end
308
238
  end
309
- @thread.join
310
239
  end
240
+ end
311
241
 
312
- def shutdown
313
- @finished = true
314
- @mutex.synchronize do
315
- kill_child(60) # TODO wait time
316
- end
317
- end
242
+ def terminate
243
+ @children = []
244
+ super
245
+ end
318
246
 
319
- def write(chunk)
320
- begin
321
- chunk.write_to(@io)
322
- rescue Errno::EPIPE => e
323
- # Broken pipe (child process unexpectedly exited)
324
- @log.warn "exec_filter Broken pipe, child process maybe exited.", command: @command
325
- if try_respawn
326
- retry # retry chunk#write_to with child respawned
327
- else
328
- raise e # to retry #write with other ChildProcess instance (when num_children > 1)
329
- end
247
+ def tag_remove_prefix(tag)
248
+ if @remove_prefix
249
+ if (tag[0, @removed_length] == @removed_prefix_string and tag.length > @removed_length) or tag == @removed_prefix_string
250
+ tag = tag[@removed_length..-1] || ''
330
251
  end
331
252
  end
253
+ tag
254
+ end
332
255
 
333
- def try_respawn
334
- return false if @respawns == 0
335
- @mutex.synchronize do
336
- return false if @respawns == 0
337
-
338
- kill_child(5) # TODO wait time
339
-
340
- @io = IO.popen(@command, "r+")
341
- @pid = @io.pid
342
- @io.sync = true
343
- @thread = Thread.new(&method(:run))
256
+ NEWLINE = "\n"
344
257
 
345
- @respawns -= 1 if @respawns > 0
346
- end
347
- @log.warn "exec_filter child process successfully respawned.", command: @command, respawns: @respawns
348
- true
258
+ def format(tag, time, record)
259
+ tag = tag_remove_prefix(tag)
260
+ record = inject_values_to_record(tag, time, record)
261
+ if @formatter.formatter_type == :text_per_line
262
+ @formatter.format(tag, time, record).chomp + NEWLINE
263
+ else
264
+ @formatter.format(tag, time, record)
349
265
  end
266
+ end
350
267
 
351
- def run
352
- @parser.call(@io)
353
- rescue
354
- @log.error "exec_filter thread unexpectedly failed with an error.", command: @command, error: $!.to_s
355
- @log.warn_backtrace $!.backtrace
356
- ensure
357
- _pid, stat = Process.waitpid2(@pid)
358
- unless @finished
359
- @log.error "exec_filter process unexpectedly exited.", command: @command, ecode: stat.to_i
360
- unless @respawns == 0
361
- @log.warn "exec_filter child process will respawn for next input data (respawns #{@respawns})."
362
- end
268
+ def write(chunk)
269
+ try_times = 0
270
+ while true
271
+ r = @rr = (@rr + 1) % @children.length
272
+ if @children[r].pid && writeio = @children[r].writeio
273
+ chunk.write_to(writeio)
274
+ break
363
275
  end
276
+ try_times += 1
277
+ raise "no healthy child processes exist" if try_times >= @children.length
364
278
  end
365
279
  end
366
280
 
367
- def on_message(record)
368
- if val = record.delete(@out_time_key)
369
- time = @time_parse_proc.call(val)
370
- else
371
- time = Engine.now
372
- end
373
-
374
- if val = record.delete(@out_tag_key)
375
- tag = if @add_prefix
376
- @added_prefix_string + val
377
- else
378
- val
379
- end
281
+ def run(io)
282
+ case
283
+ when @parser.implement?(:parse_io)
284
+ @parser.parse_io(io, &method(:on_record))
285
+ when @parser.implement?(:parse_partial_data)
286
+ until io.eof?
287
+ @parser.parse_partial_data(io.readpartial(@read_block_size), &method(:on_record))
288
+ end
289
+ when @parser.parser_type == :text_per_line
290
+ io.each_line do |line|
291
+ @parser.parse(line.chomp, &method(:on_record))
292
+ end
380
293
  else
381
- tag = @tag
294
+ @parser.parse(io.read, &method(:on_record))
382
295
  end
296
+ end
383
297
 
298
+ def on_record(time, record)
299
+ tag = extract_tag_from_record(record)
300
+ tag = @added_prefix_string + tag if tag && @add_prefix
301
+ tag ||= @tag
302
+ time ||= extract_time_from_record(record) || Fluent::EventTime.now
384
303
  router.emit(tag, time, record)
385
- rescue
304
+ rescue => e
386
305
  if @suppress_error_log_interval == 0 || Time.now.to_i > @next_log_time
387
- log.error "exec_filter failed to emit", error: $!, record: Yajl.dump(record)
388
- log.warn_backtrace $!.backtrace
306
+ log.error "exec_filter failed to emit", record: Yajl.dump(record), error: e
307
+ log.error_backtrace e.backtrace
389
308
  @next_log_time = Time.now.to_i + @suppress_error_log_interval
390
309
  end
310
+ router.emit_error_event(tag, time, record, e) if tag && time && record
391
311
  end
392
312
  end
393
313
  end