fluent-plugin-splunk-hec 1.1.2 → 1.2.0

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.
data/Rakefile CHANGED
@@ -1,5 +1,10 @@
1
+ # frozen_string_literal: true
2
+
1
3
  require 'bundler/gem_tasks'
2
4
  require 'rake/testtask'
5
+ require 'rubocop/rake_task'
6
+
7
+ RuboCop::RakeTask.new
3
8
 
4
9
  Rake::TestTask.new(:test) do |t|
5
10
  t.libs << 'test'
@@ -9,4 +14,4 @@ Rake::TestTask.new(:test) do |t|
9
14
  t.warning = false
10
15
  end
11
16
 
12
- task :default => :test
17
+ task default: :test
data/VERSION CHANGED
@@ -1 +1 @@
1
- 1.1.2
1
+ 1.2.0
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  Gem::Specification.new do |spec|
2
4
  spec.name = 'fluent-plugin-splunk-hec'
3
5
  spec.version = File.read('VERSION')
@@ -31,15 +33,19 @@ Gem::Specification.new do |spec|
31
33
 
32
34
  spec.required_ruby_version = '>= 2.3.0'
33
35
 
34
- spec.add_runtime_dependency 'fluentd', '~> 1.4'
36
+ spec.add_runtime_dependency 'fluent-plugin-kubernetes_metadata_filter', '= 2.1.2'
37
+ spec.add_runtime_dependency 'fluentd', '= 1.4'
35
38
  spec.add_runtime_dependency 'multi_json', '~> 1.13'
36
39
  spec.add_runtime_dependency 'net-http-persistent', '~> 3.0'
40
+ spec.add_runtime_dependency 'openid_connect', '~> 1.1.6'
41
+ spec.add_runtime_dependency 'prometheus-client', '~> 0.9.0'
37
42
 
38
43
  spec.add_development_dependency 'bundler', '~> 2.0'
39
44
  spec.add_development_dependency 'rake', '~> 12.0'
40
45
  # required by fluent/test.rb
41
- spec.add_development_dependency 'test-unit', '~> 3.0'
42
46
  spec.add_development_dependency 'minitest', '~> 5.0'
43
- spec.add_development_dependency 'webmock', '~> 3.5.0'
47
+ spec.add_development_dependency 'rubocop', '~> 0.63.1'
44
48
  spec.add_development_dependency 'simplecov', '~> 0.16.1'
49
+ spec.add_development_dependency 'test-unit', '~> 3.0'
50
+ spec.add_development_dependency 'webmock', '~> 3.5.0'
45
51
  end
@@ -0,0 +1,313 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'fluent/output'
4
+ require 'fluent/plugin/output'
5
+ require 'fluent/plugin/formatter'
6
+ require 'prometheus/client'
7
+ require 'benchmark'
8
+
9
+ module Fluent::Plugin
10
+ class SplunkOutput < Fluent::BufferedOutput
11
+ helpers :formatter
12
+
13
+ autoload :VERSION, 'fluent/plugin/out_splunk/version'
14
+ autoload :MatchFormatter, 'fluent/plugin/out_splunk/match_formatter'
15
+
16
+ KEY_FIELDS = %w[index host source sourcetype metric_name metric_value].freeze
17
+ TAG_PLACEHOLDER = '${tag}'
18
+
19
+ desc 'The host field for events, by default it uses the hostname of the machine that runnning fluentd. This is exclusive with `host_key`.'
20
+ config_param :host, :string, default: nil
21
+
22
+ desc 'Field name to contain host. This is exclusive with `host`.'
23
+ config_param :host_key, :string, default: nil
24
+
25
+ desc 'The source field for events, when not set, will be decided by HEC. This is exclusive with `source_key`.'
26
+ config_param :source, :string, default: nil
27
+
28
+ desc 'Field name to contain source. This is exclusive with `source`.'
29
+ config_param :source_key, :string, default: nil
30
+
31
+ desc 'The sourcetype field for events, when not set, will be decided by HEC. This is exclusive with `sourcetype_key`.'
32
+ config_param :sourcetype, :string, default: nil
33
+
34
+ desc 'Field name to contain sourcetype. This is exclusive with `sourcetype`.'
35
+ config_param :sourcetype_key, :string, default: nil
36
+
37
+ desc 'Field name to contain Splunk event time. By default will use fluentd\'d time'
38
+ config_param :time_key, :string, default: nil
39
+
40
+ desc 'The Splunk index to index events. When not set, will be decided by HEC. This is exclusive with `index_key`'
41
+ config_param :index, :string, default: nil
42
+
43
+ desc 'Field name to contain Splunk index name. This is exclusive with `index`.'
44
+ config_param :index_key, :string, default: nil
45
+
46
+ desc 'When set to true, all fields defined in `index_key`, `host_key`, `source_key`, `sourcetype_key`, `metric_name_key`, `metric_value_key` will not be removed from the original event.'
47
+ config_param :keep_keys, :bool, default: false
48
+
49
+ desc 'Define index-time fields for event data type, or metric dimensions for metric data type. Null value fields will be removed.'
50
+ config_section :fields, init: false, multi: false, required: false do
51
+ # this is blank on purpose
52
+ end
53
+
54
+ config_section :format do
55
+ config_set_default :usage, '**'
56
+ config_set_default :@type, 'json'
57
+ config_set_default :add_newline, false
58
+ end
59
+
60
+ desc <<~DESC
61
+ Whether to allow non-UTF-8 characters in user logs. If set to true, any
62
+ non-UTF-8 character would be replaced by the string specified by
63
+ `non_utf8_replacement_string`. If set to false, any non-UTF-8 character
64
+ would trigger the plugin to error out.
65
+ DESC
66
+ config_param :coerce_to_utf8, :bool, default: true
67
+
68
+ desc <<~DESC
69
+ If `coerce_to_utf8` is set to true, any non-UTF-8 character would be
70
+ replaced by the string specified here.
71
+ DESC
72
+ config_param :non_utf8_replacement_string, :string, default: ' '
73
+
74
+ def initialize
75
+ super
76
+ @registry = ::Prometheus::Client.registry
77
+ end
78
+
79
+ def configure(conf)
80
+ super
81
+ check_conflict
82
+ @api = construct_api
83
+ prepare_key_fields
84
+ configure_fields(conf)
85
+ configure_metrics(conf)
86
+
87
+ # @formatter_configs is from formatter helper
88
+ @formatters = @formatter_configs.map do |section|
89
+ MatchFormatter.new section.usage, formatter_create(usage: section.usage)
90
+ end
91
+ end
92
+
93
+ def write(chunk)
94
+ log.trace { "#{self.class}: Received new chunk, size=#{chunk.read.bytesize}" }
95
+
96
+ t = Benchmark.realtime do
97
+ write_to_splunk(chunk)
98
+ end
99
+
100
+ @metrics[:record_counter].increment(metric_labels, chunk.size_of_events)
101
+ @metrics[:bytes_counter].increment(metric_labels, chunk.bytesize)
102
+ @metrics[:write_records_histogram].observe(metric_labels, chunk.size_of_events)
103
+ @metrics[:write_bytes_histogram].observe(metric_labels, chunk.bytesize)
104
+ @metrics[:write_latency_histogram].observe(metric_labels, t)
105
+ end
106
+
107
+ def write_to_splunk(_chunk)
108
+ raise NotImplementedError("Child class should implement 'write_to_splunk'")
109
+ end
110
+
111
+ def construct_api
112
+ raise NotImplementedError("Child class should implement 'construct_api'")
113
+ end
114
+
115
+ protected
116
+
117
+ def prepare_event_payload(tag, time, record)
118
+ {
119
+ host: @host ? @host.call(tag, record) : @default_host,
120
+ # From the API reference
121
+ # http://docs.splunk.com/Documentation/Splunk/latest/RESTREF/RESTinput#services.2Fcollector
122
+ # `time` should be a string or unsigned integer.
123
+ # That's why we use `to_s` here.
124
+ time: time.to_f.to_s
125
+ }.tap do |payload|
126
+ payload[:index] = @index.call(tag, record) if @index
127
+ payload[:source] = @source.call(tag, record) if @source
128
+ payload[:sourcetype] = @sourcetype.call(tag, record) if @sourcetype
129
+
130
+ # delete nil fields otherwise will get format error from HEC
131
+ %i[host index source sourcetype].each { |f| payload.delete f if payload[f].nil? }
132
+
133
+ if @extra_fields
134
+ payload[:fields] = @extra_fields.map { |name, field| [name, record[field]] }.to_h
135
+ payload[:fields].compact!
136
+ # if a field is already in indexed fields, then remove it from the original event
137
+ @extra_fields.values.each { |field| record.delete field }
138
+ end
139
+ if formatter = @formatters.find { |f| f.match? tag }
140
+ record = formatter.format(tag, time, record)
141
+ end
142
+ payload[:event] = convert_to_utf8 record
143
+ end
144
+ end
145
+
146
+ def format_event(tag, time, record)
147
+ MultiJson.dump(prepare_event_payload(tag, time, record))
148
+ end
149
+
150
+ def process_response(response, _request_body)
151
+ log.trace { "[Response] POST #{@api}: #{response.inspect}" }
152
+
153
+ @metrics[:status_counter].increment(metric_labels(status: response.code.to_s))
154
+
155
+ # raise Exception to utilize Fluentd output plugin retry mechanism
156
+ raise "Server error (#{response.code}) for POST #{@api}, response: #{response.body}" if response.code.to_s.start_with?('5')
157
+
158
+ # For both success response (2xx) and client errors (4xx), we will consume the chunk.
159
+ # Because there probably a bug in the code if we get 4xx errors, retry won't do any good.
160
+ unless response.code.to_s.start_with?('2')
161
+ log.error "#{self.class}: Failed POST to #{@api}, response: #{response.body}"
162
+ log.error { "#{self.class}: Failed request body: #{post.body}" }
163
+ end
164
+ end
165
+
166
+ private
167
+
168
+ def check_conflict
169
+ KEY_FIELDS.each do |f|
170
+ kf = "#{f}_key"
171
+ raise Fluent::ConfigError, "Can not set #{f} and #{kf} at the same time." \
172
+ if %W[@#{f} @#{kf}].all? &method(:instance_variable_get)
173
+ end
174
+ end
175
+
176
+ def prepare_key_fields
177
+ KEY_FIELDS.each do |f|
178
+ v = instance_variable_get "@#{f}_key"
179
+ if v
180
+ attrs = v.split('.').freeze
181
+ if @keep_keys
182
+ instance_variable_set "@#{f}", ->(_, record) { attrs.inject(record) { |o, k| o[k] } }
183
+ else
184
+ instance_variable_set "@#{f}", lambda { |_, record|
185
+ attrs[0...-1].inject(record) { |o, k| o[k] }.delete(attrs[-1])
186
+ }
187
+ end
188
+ else
189
+ v = instance_variable_get "@#{f}"
190
+ next unless v
191
+
192
+ if v == TAG_PLACEHOLDER
193
+ instance_variable_set "@#{f}", ->(tag, _) { tag }
194
+ else
195
+ instance_variable_set "@#{f}", ->(_, _) { v }
196
+ end
197
+ end
198
+ end
199
+ end
200
+
201
+ # <fields> directive, which defines:
202
+ # * when data_type is event, index-time fields
203
+ # * when data_type is metric, metric dimensions
204
+ def configure_fields(conf)
205
+ # This loop looks dump, but it is used to suppress the unused parameter configuration warning
206
+ # Learned from `filter_record_transformer`.
207
+ conf.elements.select { |element| element.name == 'fields' }.each do |element|
208
+ element.each_pair { |k, _v| element.key?(k) }
209
+ end
210
+
211
+ return unless @fields
212
+
213
+ @extra_fields = @fields.corresponding_config_element.map do |k, v|
214
+ [k, v.empty? ? k : v]
215
+ end.to_h
216
+ end
217
+
218
+ def pick_custom_format_method
219
+ if @data_type == :event
220
+ define_singleton_method :format, method(:format_event)
221
+ else
222
+ define_singleton_method :format, method(:format_metric)
223
+ end
224
+ end
225
+
226
+ def configure_metrics(conf)
227
+ @metric_labels = {
228
+ type: conf['@type'],
229
+ plugin_id: plugin_id
230
+ }
231
+
232
+ @metrics = {
233
+ record_counter: register_metric(::Prometheus::Client::Counter.new(
234
+ :splunk_output_write_records_count,
235
+ 'The number of log records being sent'
236
+ )),
237
+ bytes_counter: register_metric(::Prometheus::Client::Counter.new(
238
+ :splunk_output_write_bytes_count,
239
+ 'The number of log bytes being sent'
240
+ )),
241
+ status_counter: register_metric(::Prometheus::Client::Counter.new(
242
+ :splunk_output_write_status_count,
243
+ 'The count of sends by response_code'
244
+ )),
245
+ write_bytes_histogram: register_metric(::Prometheus::Client::Histogram.new(
246
+ :splunk_output_write_payload_bytes,
247
+ 'The size of the write payload in bytes', {}, [1024, 23_937, 47_875, 95_750, 191_500, 383_000, 766_000, 1_149_000]
248
+ )),
249
+ write_records_histogram: register_metric(::Prometheus::Client::Histogram.new(
250
+ :splunk_output_write_payload_records,
251
+ 'The number of records written per write', {}, [1, 10, 25, 100, 200, 300, 500, 750, 1000, 1500]
252
+ )),
253
+ write_latency_histogram: register_metric(::Prometheus::Client::Histogram.new(
254
+ :splunk_output_write_latency_seconds,
255
+ 'The latency of writes'
256
+ ))
257
+ }
258
+ end
259
+
260
+ # Tag metrics with the type string that was used to register the plugin
261
+ def metric_labels(other_labels = {})
262
+ @metric_labels.merge other_labels
263
+ end
264
+
265
+ # Encode as UTF-8. If 'coerce_to_utf8' is set to true in the config, any
266
+ # non-UTF-8 character would be replaced by the string specified by
267
+ # 'non_utf8_replacement_string'. If 'coerce_to_utf8' is set to false, any
268
+ # non-UTF-8 character would trigger the plugin to error out.
269
+ # Thanks to
270
+ # https://github.com/GoogleCloudPlatform/fluent-plugin-google-cloud/blob/dbc28575/lib/fluent/plugin/out_google_cloud.rb#L1284
271
+ def convert_to_utf8(input)
272
+ if input.is_a?(Hash)
273
+ record = {}
274
+ input.each do |key, value|
275
+ record[convert_to_utf8(key)] = convert_to_utf8(value)
276
+ end
277
+
278
+ return record
279
+ end
280
+ return input.map { |value| convert_to_utf8(value) } if input.is_a?(Array)
281
+ return input unless input.respond_to?(:encode)
282
+
283
+ if @coerce_to_utf8
284
+ input.encode(
285
+ 'utf-8',
286
+ invalid: :replace,
287
+ undef: :replace,
288
+ replace: @non_utf8_replacement_string
289
+ )
290
+ else
291
+ begin
292
+ input.encode('utf-8')
293
+ rescue EncodingError
294
+ log.error do
295
+ 'Encountered encoding issues potentially due to non ' \
296
+ 'UTF-8 characters. To allow non-UTF-8 characters and ' \
297
+ 'replace them with spaces, please set "coerce_to_utf8" ' \
298
+ 'to true.'
299
+ end
300
+ raise
301
+ end
302
+ end
303
+ end
304
+
305
+ def register_metric(metric)
306
+ if !@registry.exist?(metric.name)
307
+ @registry.register(metric)
308
+ else
309
+ @registry.get(metric.name)
310
+ end
311
+ end
312
+ end
313
+ end
@@ -1,11 +1,13 @@
1
+ # frozen_string_literal: true
2
+
1
3
  require 'fluent/match'
2
4
 
3
- class Fluent::Plugin::SplunkHecOutput::MatchFormatter
5
+ class Fluent::Plugin::SplunkOutput::MatchFormatter
4
6
  def initialize(pattern, formatter)
5
7
  # based on fluentd/lib/fluent/event_router.rb
6
- patterns = pattern.split(/\s+/).map { |str|
8
+ patterns = pattern.split(/\s+/).map do |str|
7
9
  Fluent::MatchPattern.create(str)
8
- }
10
+ end
9
11
  @pattern =
10
12
  if patterns.length == 1
11
13
  patterns[0]
@@ -0,0 +1,3 @@
1
+ # frozen_string_literal: true
2
+
3
+ Fluent::Plugin::SplunkOutput::VERSION = File.read(File.expand_path('../../../../VERSION', File.dirname(__FILE__))).chomp.strip
@@ -1,14 +1,16 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require "fluent/plugin/output"
4
- require "fluent/plugin/formatter"
3
+ require 'fluent/output'
4
+ require 'fluent/plugin/output'
5
+ require 'fluent/plugin/formatter'
6
+ require 'fluent/plugin/out_splunk'
5
7
 
6
8
  require 'openssl'
7
9
  require 'multi_json'
8
10
  require 'net/http/persistent'
9
11
 
10
12
  module Fluent::Plugin
11
- class SplunkHecOutput < Fluent::Plugin::Output
13
+ class SplunkHecOutput < SplunkOutput
12
14
  Fluent::Plugin.register_output('splunk_hec', self)
13
15
 
14
16
  helpers :formatter
@@ -19,10 +21,10 @@ module Fluent::Plugin
19
21
  KEY_FIELDS = %w[index time host source sourcetype metric_name metric_value].freeze
20
22
  TAG_PLACEHOLDER = '${tag}'.freeze
21
23
 
22
- MISSING_FIELD = Hash.new { |h, k|
24
+ MISSING_FIELD = Hash.new do |_h, k|
23
25
  $log.warn "expected field #{k} but it's missing" if defined?($log)
24
26
  MISSING_FIELD
25
- }.freeze
27
+ end.freeze
26
28
 
27
29
  desc 'Protocol to use to call HEC API.'
28
30
  config_param :protocol, :enum, list: %i[http https], default: :https
@@ -75,31 +77,13 @@ module Fluent::Plugin
75
77
  desc 'Field name to contain Splunk index name. This is exclusive with `index`.'
76
78
  config_param :index_key, :string, default: nil
77
79
 
78
- desc "The host field for events, by default it uses the hostname of the machine that runnning fluentd. This is exclusive with `host_key`."
79
- config_param :host, :string, default: nil
80
-
81
- desc 'Field name to contain host. This is exclusive with `host`.'
82
- config_param :host_key, :string, default: nil
83
-
84
- desc 'The source field for events, when not set, will be decided by HEC. This is exclusive with `source_key`.'
85
- config_param :source, :string, default: nil
86
-
87
- desc 'Field name to contain source. This is exclusive with `source`.'
88
- config_param :source_key, :string, default: nil
89
-
90
- desc 'The sourcetype field for events, when not set, will be decided by HEC. This is exclusive with `sourcetype_key`.'
91
- config_param :sourcetype, :string, default: nil
92
-
93
- desc 'Field name to contain sourcetype. This is exclusive with `sourcetype`.'
94
- config_param :sourcetype_key, :string, default: nil
95
-
96
80
  desc 'When `data_type` is set to "metric", by default it will treat every key-value pair in the income event as a metric name-metric value pair. Set `metrics_from_event` to `false` to disable this behavior and use `metric_name_key` and `metric_value_key` to define metrics.'
97
81
  config_param :metrics_from_event, :bool, default: true
98
82
 
99
- desc "Field name to contain metric name. This is exclusive with `metrics_from_event`, when this is set, `metrics_from_event` will be set to `false`."
83
+ desc 'Field name to contain metric name. This is exclusive with `metrics_from_event`, when this is set, `metrics_from_event` will be set to `false`.'
100
84
  config_param :metric_name_key, :string, default: nil
101
85
 
102
- desc "Field name to contain metric value, this is required when `metric_name_key` is set."
86
+ desc 'Field name to contain metric value, this is required when `metric_name_key` is set.'
103
87
  config_param :metric_value_key, :string, default: nil
104
88
 
105
89
  desc 'When set to true, all fields defined in `index_key`, `host_key`, `source_key`, `sourcetype_key`, `metric_name_key`, `metric_value_key` will not be removed from the original event.'
@@ -109,7 +93,7 @@ module Fluent::Plugin
109
93
  config_section :fields, init: false, multi: false, required: false do
110
94
  # this is blank on purpose
111
95
  end
112
-
96
+
113
97
  config_section :format do
114
98
  config_set_default :usage, '**'
115
99
  config_set_default :@type, 'json'
@@ -139,47 +123,42 @@ module Fluent::Plugin
139
123
  def configure(conf)
140
124
  super
141
125
 
142
- check_conflict
143
126
  check_metric_configs
144
- construct_api
145
- prepare_key_fields
146
- configure_fields(conf)
147
127
  pick_custom_format_method
148
-
149
- # @formatter_configs is from formatter helper
150
- @formatters = @formatter_configs.map { |section|
151
- MatchFormatter.new section.usage, formatter_create(usage: section.usage)
152
- }
153
128
  end
154
129
 
155
130
  def start
156
131
  super
132
+ @conn = Net::HTTP::Persistent.new.tap do |c|
133
+ c.verify_mode = @insecure_ssl ? OpenSSL::SSL::VERIFY_NONE : OpenSSL::SSL::VERIFY_PEER
134
+ c.cert = OpenSSL::X509::Certificate.new File.read(@client_cert) if @client_cert
135
+ c.key = OpenSSL::PKey::RSA.new File.read(@client_key) if @client_key
136
+ c.ca_file = @ca_file
137
+ c.ca_path = @ca_path
138
+ c.ciphers = @ssl_ciphers
157
139
 
158
- @hec_conn = new_connection
140
+ c.override_headers['Content-Type'] = 'application/json'
141
+ c.override_headers['User-Agent'] = "fluent-plugin-splunk_hec_out/#{VERSION}"
142
+ c.override_headers['Authorization'] = "Splunk #{@hec_token}"
143
+ end
159
144
  end
160
145
 
161
- def format(tag, time, record)
162
- # this method will be replaced in `configure`
146
+ def shutdown
147
+ super
148
+ @conn.shutdown
163
149
  end
164
150
 
165
- def write(chunk)
166
- log.debug { "Received new chunk, size=#{chunk.read.bytesize}" }
167
- send_to_hec chunk
151
+ def format(tag, time, record)
152
+ # this method will be replaced in `configure`
168
153
  end
169
154
 
170
155
  def multi_workers_ready?
171
156
  true
172
157
  end
173
158
 
174
- private
159
+ protected
175
160
 
176
- def check_conflict
177
- KEY_FIELDS.each { |f|
178
- kf = "#{f}_key"
179
- raise Fluent::ConfigError, "Can not set #{f} and #{kf} at the same time." \
180
- if %W[@#{f} @#{kf}].all? &method(:instance_variable_get)
181
- }
182
- end
161
+ private
183
162
 
184
163
  def check_metric_configs
185
164
  return unless @data_type == :metric
@@ -188,71 +167,57 @@ module Fluent::Plugin
188
167
 
189
168
  return if @metrics_from_event
190
169
 
191
- raise Fluent::ConfigError, "`metric_name_key` is required when `metrics_from_event` is `false`." unless @metric_name_key
192
-
193
- raise Fluent::ConfigError, "`metric_value_key` is required when `metric_name_key` is set." unless @metric_value_key
170
+ raise Fluent::ConfigError, '`metric_name_key` is required when `metrics_from_event` is `false`.' unless @metric_name_key
171
+ raise Fluent::ConfigError, '`metric_value_key` is required when `metric_name_key` is set.' unless @metric_value_key
194
172
  end
195
173
 
196
- def prepare_key_fields
197
- KEY_FIELDS.each { |f|
198
- v = instance_variable_get "@#{f}_key"
199
- if v
200
- attrs = v.split('.').freeze
201
- if @keep_keys
202
- instance_variable_set "@#{f}", ->(_, record) { attrs.inject(record) { |o, k| o[k] } }
203
- else
204
- instance_variable_set "@#{f}", ->(_, record) {
205
- attrs[0...-1].inject(record) { |o, k| o[k] }.delete(attrs[-1])
206
- }
207
- end
208
- else
209
- v = instance_variable_get "@#{f}"
210
- next unless v
211
-
212
- if v == TAG_PLACEHOLDER
213
- instance_variable_set "@#{f}", ->(tag, _) { tag }
214
- else
215
- instance_variable_set "@#{f}", ->(_, _) { v }
216
- end
217
- end
218
- }
219
- end
220
-
221
- # <fields> directive, which defines:
222
- # * when data_type is event, index-time fields
223
- # * when data_type is metric, metric dimensions
224
- def configure_fields(conf)
225
- # This loop looks dump, but it is used to suppress the unused parameter configuration warning
226
- # Learned from `filter_record_transformer`.
227
- conf.elements.select { |element| element.name == 'fields' }.each do |element|
228
- element.each_pair { |k, v| element.has_key?(k) }
229
- end
174
+ def format_event(tag, time, record)
175
+ MultiJson.dump({
176
+ host: @host ? @host.(tag, record) : @default_host,
177
+ # From the API reference
178
+ # http://docs.splunk.com/Documentation/Splunk/latest/RESTREF/RESTinput#services.2Fcollector
179
+ # `time` should be a string or unsigned integer.
180
+ # That's why we use the to_string function here.
181
+ time: time.to_f.to_s
182
+ }.tap { |payload|
183
+ if @time
184
+ time_value = @time.(tag, record)
185
+ # if no value is found don't override and use fluentd's time
186
+ if !time_value.nil?
187
+ payload[:time] = time_value
188
+ end
189
+ end
230
190
 
231
- return unless @fields
191
+ payload[:index] = @index.(tag, record) if @index
192
+ payload[:source] = @source.(tag, record) if @source
193
+ payload[:sourcetype] = @sourcetype.(tag, record) if @sourcetype
232
194
 
233
- @extra_fields = @fields.corresponding_config_element.map { |k, v|
234
- [k, v.empty? ? k : v]
235
- }.to_h
236
- end
195
+ # delete nil fields otherwise will get formet error from HEC
196
+ %i[host index source sourcetype].each { |f| payload.delete f if payload[f].nil? }
237
197
 
238
- def pick_custom_format_method
239
- if @data_type == :event
240
- define_singleton_method :format, method(:format_event)
241
- else
242
- define_singleton_method :format, method(:format_metric)
243
- end
198
+ if @extra_fields
199
+ payload[:fields] = @extra_fields.map { |name, field| [name, record[field]] }.to_h
200
+ payload[:fields].delete_if { |_k,v| v.nil? }
201
+ # if a field is already in indexed fields, then remove it from the original event
202
+ @extra_fields.values.each { |field| record.delete field }
203
+ end
204
+ if formatter = @formatters.find { |f| f.match? tag }
205
+ record = formatter.format(tag, time, record)
206
+ end
207
+ payload[:event] = convert_to_utf8 record
208
+ })
244
209
  end
245
210
 
246
- def format_event(tag, time, record)
247
- MultiJson.dump({
248
- host: @host ? @host.(tag, record) : @default_host,
249
- # From the API reference
250
- # http://docs.splunk.com/Documentation/Splunk/latest/RESTREF/RESTinput#services.2Fcollector
251
- # `time` should be a string or unsigned integer.
252
- # That's why we use the to_string function here.
253
- time: time.to_f.to_s
254
- }.tap { |payload|
255
-
211
+ def format_metric(tag, time, record)
212
+ payload = {
213
+ host: @host ? @host.call(tag, record) : @default_host,
214
+ # From the API reference
215
+ # http://docs.splunk.com/Documentation/Splunk/latest/RESTREF/RESTinput#services.2Fcollector
216
+ # `time` should be a string or unsigned integer.
217
+ # That's why we use `to_s` here.
218
+ time: time.to_f.to_s,
219
+ event: 'metric'
220
+ }.tap do |payload|
256
221
  if @time
257
222
  time_value = @time.(tag, record)
258
223
  # if no value is found don't override and use fluentd's time
@@ -260,71 +225,42 @@ module Fluent::Plugin
260
225
  payload[:time] = time_value
261
226
  end
262
227
  end
228
+ end
229
+ payload[:index] = @index.call(tag, record) if @index
230
+ payload[:source] = @source.call(tag, record) if @source
231
+ payload[:sourcetype] = @sourcetype.call(tag, record) if @sourcetype
232
+
233
+ unless @metrics_from_event
234
+ fields = {
235
+ metric_name: @metric_name.call(tag, record),
236
+ _value: @metric_value.call(tag, record)
237
+ }
238
+
239
+ if @extra_fields
240
+ fields.update @extra_fields.map { |name, field| [name, record[field]] }.to_h
241
+ fields.delete_if { |_k,v| v.nil? }
242
+ else
243
+ fields.update record
244
+ end
263
245
 
264
- payload[:index] = @index.(tag, record) if @index
265
- payload[:source] = @source.(tag, record) if @source
266
- payload[:sourcetype] = @sourcetype.(tag, record) if @sourcetype
267
-
268
- # delete nil fields otherwise will get formet error from HEC
269
- %i[host index source sourcetype].each { |f| payload.delete f if payload[f].nil? }
270
-
271
- if @extra_fields
272
- payload[:fields] = @extra_fields.map { |name, field| [name, record[field]] }.to_h
273
- payload[:fields].delete_if { |_k,v| v.nil? }
274
- # if a field is already in indexed fields, then remove it from the original event
275
- @extra_fields.values.each { |field| record.delete field }
276
- end
277
- if formatter = @formatters.find { |f| f.match? tag }
278
- record = formatter.format(tag, time, record)
279
- end
280
- payload[:event] = convert_to_utf8 record
281
- })
282
- end
246
+ fields.delete_if { |_k,v| v.nil? }
283
247
 
284
- def format_metric(tag, time, record)
285
- payload = {
286
- host: @host ? @host.(tag, record) : @default_host,
287
- # From the API reference
288
- # http://docs.splunk.com/Documentation/Splunk/latest/RESTREF/RESTinput#services.2Fcollector
289
- # `time` should be a string or unsigned integer.
290
- # That's why we use `to_s` here.
291
- time: time.to_f.to_s,
292
- event: 'metric'
293
- }
294
- payload[:index] = @index.(tag, record) if @index
295
- payload[:source] = @source.(tag, record) if @source
296
- payload[:sourcetype] = @sourcetype.(tag, record) if @sourcetype
297
-
298
- if not @metrics_from_event
299
- fields = {
300
- metric_name: @metric_name.(tag, record),
301
- _value: @metric_value.(tag, record)
302
- }
303
-
304
- if @extra_fields
305
- fields.update @extra_fields.map { |name, field| [name, record[field]] }.to_h
306
- else
307
- fields.update record
308
- end
309
-
310
- fields.delete_if { |_k,v| v.nil? }
311
-
312
- payload[:fields] = convert_to_utf8 fields
313
-
314
- return MultiJson.dump(payload)
248
+ payload[:fields] = convert_to_utf8 fields
249
+
250
+ return MultiJson.dump(payload)
315
251
  end
316
252
 
317
253
  # when metrics_from_event is true, generate one metric event for each key-value in record
318
- payloads = record.map { |key, value|
319
- {fields: {metric_name: key, _value: value}}.merge! payload
320
- }
254
+ payloads = record.map do |key, value|
255
+ { fields: { metric_name: key, _value: value } }.merge! payload
256
+ end
321
257
 
322
258
  payloads.map!(&MultiJson.method(:dump)).join
323
259
  end
324
260
 
325
261
  def construct_api
326
- @hec_api = URI("#{@protocol}://#{@hec_host}:#{@hec_port}/services/collector")
327
- rescue
262
+ URI("#{@protocol}://#{@hec_host}:#{@hec_port}/services/collector")
263
+ rescue StandardError
328
264
  raise Fluent::ConfigError, "hec_host (#{@hec_host}) and/or hec_port (#{@hec_port}) are invalid."
329
265
  end
330
266
 
@@ -346,14 +282,15 @@ module Fluent::Plugin
346
282
  end
347
283
  end
348
284
 
349
- def send_to_hec(chunk)
350
- post = Net::HTTP::Post.new @hec_api.request_uri
285
+ def write_to_splunk(chunk)
286
+ post = Net::HTTP::Post.new @api.request_uri
351
287
  post.body = chunk.read
352
- log.debug { "Sending #{post.body.bytesize} bytes to Splunk." }
288
+ log.debug { "[Sending] Chunk: #{dump_unique_id_hex(chunk.unique_id)}(#{post.body.bytesize}B)." }
289
+ log.trace { "POST #{@api} body=#{post.body}" }
353
290
 
354
- log.trace { "POST #{@hec_api} body=#{post.body}" }
355
- response = @hec_conn.request @hec_api, post
356
- log.debug { "[Response] POST #{@hec_api}: #{response.inspect}" }
291
+ t1 = Time.now
292
+ response = @conn.request @api, post
293
+ t2 = Time.now
357
294
 
358
295
  # raise Exception to utilize Fluentd output plugin retry machanism
359
296
  raise "Server error (#{response.code}) for POST #{@hec_api}, response: #{response.body}" if response.code.start_with?('5')
@@ -361,9 +298,12 @@ module Fluent::Plugin
361
298
  # For both success response (2xx) and client errors (4xx), we will consume the chunk.
362
299
  # Because there probably a bug in the code if we get 4xx errors, retry won't do any good.
363
300
  if not response.code.start_with?('2')
364
- log.error "Failed POST to #{@hec_api}, response: #{response.body}"
365
- log.debug { "Failed request body: #{post.body}" }
301
+ log.error "Failed POST to #{@hec_api}, response: #{response.body}"
302
+ log.debug { "Failed request body: #{post.body}" }
366
303
  end
304
+
305
+ log.debug { "[Response] Chunk: #{dump_unique_id_hex(chunk.unique_id)} Size: #{post.body.bytesize} Response: #{response.inspect} Duration: #{t2 - t1}" }
306
+ process_response(response, post.body)
367
307
  end
368
308
 
369
309
  # Encode as UTF-8. If 'coerce_to_utf8' is set to true in the config, any
@@ -374,32 +314,32 @@ module Fluent::Plugin
374
314
  # https://github.com/GoogleCloudPlatform/fluent-plugin-google-cloud/blob/dbc28575/lib/fluent/plugin/out_google_cloud.rb#L1284
375
315
  def convert_to_utf8(input)
376
316
  if input.is_a?(Hash)
377
- record = {}
378
- input.each do |key, value|
379
- record[convert_to_utf8(key)] = convert_to_utf8(value)
380
- end
317
+ record = {}
318
+ input.each do |key, value|
319
+ record[convert_to_utf8(key)] = convert_to_utf8(value)
320
+ end
381
321
 
382
- return record
322
+ return record
383
323
  end
384
324
  return input.map { |value| convert_to_utf8(value) } if input.is_a?(Array)
385
325
  return input unless input.respond_to?(:encode)
386
326
 
387
327
  if @coerce_to_utf8
388
- input.encode(
389
- 'utf-8',
390
- invalid: :replace,
391
- undef: :replace,
392
- replace: @non_utf8_replacement_string)
393
- else
394
- begin
395
- input.encode('utf-8')
396
- rescue EncodingError
397
- log.error { 'Encountered encoding issues potentially due to non ' \
398
- 'UTF-8 characters. To allow non-UTF-8 characters and ' \
399
- 'replace them with spaces, please set "coerce_to_utf8" ' \
400
- 'to true.' }
401
- raise
402
- end
328
+ input.encode(
329
+ 'utf-8',
330
+ invalid: :replace,
331
+ undef: :replace,
332
+ replace: @non_utf8_replacement_string)
333
+ else
334
+ begin
335
+ input.encode('utf-8')
336
+ rescue EncodingError
337
+ log.error { 'Encountered encoding issues potentially due to non ' \
338
+ 'UTF-8 characters. To allow non-UTF-8 characters and ' \
339
+ 'replace them with spaces, please set "coerce_to_utf8" ' \
340
+ 'to true.' }
341
+ raise
342
+ end
403
343
  end
404
344
  end
405
345
  end