fluent-plugin-splunk-hec-radiant 0.1.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.
@@ -0,0 +1,53 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "lib/fluent/plugin/splunk_hec_radiant/version"
4
+
5
+ Gem::Specification.new do |spec|
6
+ spec.name = "fluent-plugin-splunk-hec-radiant"
7
+ spec.version = Fluent::Plugin::SplunkHecRadiant::VERSION
8
+ spec.authors = ["G. Rahul Nutakki"]
9
+ spec.email = ["gnanirn@gmail.com"]
10
+
11
+ spec.summary = "Modernized Fluentd output plugin for Splunk HEC"
12
+ spec.description = "A modernized and actively maintained Fluentd output plugin for " \
13
+ "Splunk HTTP Event Collector (HEC) with updated dependencies, " \
14
+ "improved performance, and Ruby 3.x support. Forked from the " \
15
+ "original Splunk plugin with enhancements and bug fixes."
16
+ spec.homepage = "https://github.com/gnanirahulnutakki/fluent-plugin-splunk-hec-radiant"
17
+ spec.license = "Apache-2.0"
18
+ spec.required_ruby_version = ">= 3.0.0"
19
+
20
+ spec.metadata["homepage_uri"] = spec.homepage
21
+ spec.metadata["source_code_uri"] = "https://github.com/gnanirahulnutakki/fluent-plugin-splunk-hec-radiant"
22
+ spec.metadata["changelog_uri"] = "https://github.com/gnanirahulnutakki/fluent-plugin-splunk-hec-radiant/blob/main/CHANGELOG.md"
23
+ spec.metadata["bug_tracker_uri"] = "https://github.com/gnanirahulnutakki/fluent-plugin-splunk-hec-radiant/issues"
24
+ spec.metadata["documentation_uri"] = "https://github.com/gnanirahulnutakki/fluent-plugin-splunk-hec-radiant/blob/main/README.md"
25
+ spec.metadata["rubygems_mfa_required"] = "true"
26
+
27
+ # Specify which files should be added to the gem when it is released.
28
+ spec.files = Dir.glob("lib/**/*") + %w[
29
+ README.md
30
+ LICENSE
31
+ NOTICE
32
+ fluent-plugin-splunk-hec-radiant.gemspec
33
+ Gemfile
34
+ Rakefile
35
+ ]
36
+ spec.require_paths = ["lib"]
37
+
38
+ # Runtime dependencies - modernized versions
39
+ spec.add_dependency "fluentd", ">= 1.16", "< 2.0"
40
+ spec.add_dependency "net-http-persistent", ">= 4.0", "< 6.0"
41
+ spec.add_dependency "oj", "~> 3.16"
42
+ spec.add_dependency "prometheus-client", ">= 2.1.0", "< 4.0"
43
+
44
+ # Development dependencies
45
+ spec.add_development_dependency "bundler", ">= 2.0"
46
+ spec.add_development_dependency "rake", "~> 13.0"
47
+ spec.add_development_dependency "rspec", "~> 3.13"
48
+ spec.add_development_dependency "rubocop", "~> 1.60"
49
+ spec.add_development_dependency "rubocop-rspec", "~> 3.0"
50
+ spec.add_development_dependency "simplecov", "~> 0.22"
51
+ spec.add_development_dependency "test-unit", "~> 3.6"
52
+ spec.add_development_dependency "webmock", "~> 3.23"
53
+ end
@@ -0,0 +1,555 @@
1
+ # frozen_string_literal: true
2
+
3
+ # Copyright 2018 Splunk Inc.
4
+ # Modifications Copyright 2025 G. Rahul Nutakki
5
+ #
6
+ # Licensed under the Apache License, Version 2.0 (the "License");
7
+ # you may not use this file except in compliance with the License.
8
+ # You may obtain a copy of the License at
9
+ #
10
+ # http://www.apache.org/licenses/LICENSE-2.0
11
+ #
12
+ # Unless required by applicable law or agreed to in writing, software
13
+ # distributed under the License is distributed on an "AS IS" BASIS,
14
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
15
+ # See the License for the specific language governing permissions and
16
+ # limitations under the License.
17
+
18
+ require "fluent/plugin/output"
19
+ require "fluent/plugin/formatter"
20
+ require "fluent/plugin/splunk_hec_radiant/version"
21
+ require "fluent/plugin/splunk_hec_radiant/match_formatter"
22
+ require "net/http/persistent"
23
+ require "openssl"
24
+ require "oj"
25
+ require "zlib"
26
+ require "socket"
27
+ require "benchmark"
28
+ require "prometheus/client"
29
+
30
+ module Fluent
31
+ module Plugin
32
+ # Modernized Splunk HEC output plugin
33
+ class SplunkHecRadiantOutput < Fluent::Plugin::Output
34
+ Fluent::Plugin.register_output("splunk_hec_radiant", self)
35
+
36
+ helpers :formatter
37
+
38
+ KEY_FIELDS = %w[index time host source sourcetype metric_name metric_value].freeze
39
+ TAG_PLACEHOLDER = "${tag}"
40
+
41
+ MISSING_FIELD = Hash.new do |_h, k|
42
+ $log.warn "expected field #{k} but it's missing" if defined?($log)
43
+ MISSING_FIELD
44
+ end.freeze
45
+
46
+ desc "Protocol to use to call HEC API."
47
+ config_param :protocol, :enum, list: %i[http https], default: :https
48
+
49
+ desc "The hostname/IP to HEC, or HEC load balancer."
50
+ config_param :hec_host, :string, default: ""
51
+
52
+ desc "The port number to HEC, or HEC load balancer."
53
+ config_param :hec_port, :integer, default: 8088
54
+
55
+ desc "HEC REST API endpoint to use"
56
+ config_param :hec_endpoint, :string, default: "services/collector"
57
+
58
+ desc "Full url to connect to splunk. Example: https://mydomain.com:8088/apps/splunk"
59
+ config_param :full_url, :string, default: ""
60
+
61
+ desc "The HEC token."
62
+ config_param :hec_token, :string, secret: true
63
+
64
+ desc "If a connection has not been used for this number of seconds it will automatically be reset."
65
+ config_param :idle_timeout, :integer, default: 5
66
+
67
+ desc "The amount of time allowed between reading two chunks from the socket."
68
+ config_param :read_timeout, :integer, default: nil
69
+
70
+ desc "The amount of time to wait for a connection to be opened."
71
+ config_param :open_timeout, :integer, default: nil
72
+
73
+ desc "The path to a file containing a PEM-format CA certificate for this client."
74
+ config_param :client_cert, :string, default: nil
75
+
76
+ desc "The private key for this client."
77
+ config_param :client_key, :string, default: nil
78
+
79
+ desc "The path to a file containing a PEM-format CA certificate."
80
+ config_param :ca_file, :string, default: nil
81
+
82
+ desc "The path to a directory containing CA certificates in PEM format."
83
+ config_param :ca_path, :string, default: nil
84
+
85
+ desc "List of SSL ciphers allowed."
86
+ config_param :ssl_ciphers, :array, default: nil
87
+
88
+ desc "When set to true, TLS version 1.2 and above is required."
89
+ config_param :require_ssl_min_version, :bool, default: true
90
+
91
+ desc "Indicates if insecure SSL connection is allowed."
92
+ config_param :insecure_ssl, :bool, default: false
93
+
94
+ desc "Type of data sending to Splunk, `event` or `metric`."
95
+ config_param :data_type, :enum, list: %i[event metric], default: :event
96
+
97
+ desc "The Splunk index to index events."
98
+ config_param :index, :string, default: nil
99
+
100
+ desc "Field name to contain Splunk index name."
101
+ config_param :index_key, :string, default: nil
102
+
103
+ desc "The host field for events."
104
+ config_param :host, :string, default: nil
105
+
106
+ desc "Field name to contain host."
107
+ config_param :host_key, :string, default: nil
108
+
109
+ desc "The source field for events."
110
+ config_param :source, :string, default: nil
111
+
112
+ desc "Field name to contain source."
113
+ config_param :source_key, :string, default: nil
114
+
115
+ desc "The sourcetype field for events."
116
+ config_param :sourcetype, :string, default: nil
117
+
118
+ desc "Field name to contain sourcetype."
119
+ config_param :sourcetype_key, :string, default: nil
120
+
121
+ desc "Field name to contain Splunk event time."
122
+ config_param :time_key, :string, default: nil
123
+
124
+ desc "When data_type is metric, use metrics_from_event mode."
125
+ config_param :metrics_from_event, :bool, default: true
126
+
127
+ desc "Field name to contain metric name."
128
+ config_param :metric_name_key, :string, default: nil
129
+
130
+ desc "Field name to contain metric value."
131
+ config_param :metric_value_key, :string, default: nil
132
+
133
+ desc "When set to true, defined key fields will not be removed from the original event."
134
+ config_param :keep_keys, :bool, default: false
135
+
136
+ desc "Indicates if GZIP Compression is enabled."
137
+ config_param :gzip_compression, :bool, default: false
138
+
139
+ desc "App name"
140
+ config_param :app_name, :string, default: "fluent_plugin_splunk_hec_radiant"
141
+
142
+ desc "App version"
143
+ config_param :app_version, :string, default: SplunkHecRadiant::VERSION
144
+
145
+ desc "Define index-time fields for event data type, or metric dimensions for metric data type."
146
+ config_section :fields, init: false, multi: false, required: false do
147
+ # this is blank on purpose
148
+ end
149
+
150
+ desc "Indicates if 4xx errors should consume chunk"
151
+ config_param :consume_chunk_on_4xx_errors, :bool, default: true
152
+
153
+ config_section :format do
154
+ config_set_default :usage, "**"
155
+ config_set_default :@type, "json"
156
+ config_set_default :add_newline, false
157
+ end
158
+
159
+ desc "Whether to allow non-UTF-8 characters in user logs."
160
+ config_param :coerce_to_utf8, :bool, default: true
161
+
162
+ desc "If coerce_to_utf8 is true, non-UTF-8 chars are replaced with this string."
163
+ config_param :non_utf8_replacement_string, :string, default: " "
164
+
165
+ desc "Any custom headers to include alongside requests made to Splunk"
166
+ config_param :custom_headers, :hash, default: {}
167
+
168
+ def initialize
169
+ super
170
+ @default_host = Socket.gethostname
171
+ @extra_fields = nil
172
+ @registry = ::Prometheus::Client.registry
173
+ end
174
+
175
+ def configure(conf)
176
+ super
177
+ if @hec_host.empty? && @full_url.empty?
178
+ raise Fluent::ConfigError,
179
+ "One of `hec_host` or `full_url` is required."
180
+ end
181
+
182
+ check_conflict
183
+ check_metric_configs
184
+ @api = construct_api
185
+ prepare_key_fields
186
+ configure_fields(conf)
187
+ configure_metrics(conf)
188
+ pick_custom_format_method
189
+
190
+ # @formatter_configs is from formatter helper
191
+ @formatters = @formatter_configs.map do |section|
192
+ SplunkHecRadiant::MatchFormatter.new(section.usage, formatter_create(usage: section.usage))
193
+ end
194
+ end
195
+
196
+ def start
197
+ super
198
+ @conn = Net::HTTP::Persistent.new.tap do |c|
199
+ c.verify_mode = @insecure_ssl ? OpenSSL::SSL::VERIFY_NONE : OpenSSL::SSL::VERIFY_PEER
200
+ c.cert = OpenSSL::X509::Certificate.new File.read(@client_cert) if @client_cert
201
+ c.key = OpenSSL::PKey::RSA.new File.read(@client_key) if @client_key
202
+ c.ca_file = @ca_file
203
+ c.ca_path = @ca_path
204
+ c.ciphers = @ssl_ciphers
205
+ c.proxy = :ENV
206
+ c.idle_timeout = @idle_timeout
207
+ c.read_timeout = @read_timeout
208
+ c.open_timeout = @open_timeout
209
+ c.min_version = OpenSSL::SSL::TLS1_2_VERSION if @require_ssl_min_version
210
+
211
+ c.override_headers["Content-Type"] = "application/json"
212
+ c.override_headers["User-Agent"] = "fluent-plugin-splunk-hec-radiant/#{SplunkHecRadiant::VERSION}"
213
+ c.override_headers["Authorization"] = "Splunk #{@hec_token}"
214
+ c.override_headers["__splunk_app_name"] = @app_name.to_s
215
+ c.override_headers["__splunk_app_version"] = @app_version.to_s
216
+ @custom_headers.each do |header, value|
217
+ c.override_headers[header] = value
218
+ end
219
+ end
220
+ end
221
+
222
+ def shutdown
223
+ @conn&.shutdown
224
+ super
225
+ end
226
+
227
+ def format(tag, time, record)
228
+ # this method will be replaced in `configure`
229
+ end
230
+
231
+ def write(chunk)
232
+ log.trace { "#{self.class}: Received new chunk, size=#{chunk.read.bytesize}" }
233
+
234
+ t = Benchmark.realtime do
235
+ write_to_splunk(chunk)
236
+ end
237
+
238
+ @metrics[:record_counter].increment(labels: metric_labels, by: chunk.size)
239
+ @metrics[:bytes_counter].increment(labels: metric_labels, by: chunk.bytesize)
240
+ @metrics[:write_records_histogram].observe(chunk.size, labels: metric_labels)
241
+ @metrics[:write_bytes_histogram].observe(chunk.bytesize, labels: metric_labels)
242
+ @metrics[:write_latency_histogram].observe(t, labels: metric_labels)
243
+ end
244
+
245
+ def multi_workers_ready?
246
+ true
247
+ end
248
+
249
+ private
250
+
251
+ def check_conflict
252
+ KEY_FIELDS.each do |f|
253
+ kf = "#{f}_key"
254
+ next unless instance_variable_get("@#{f}") && instance_variable_get("@#{kf}")
255
+
256
+ raise Fluent::ConfigError, "Can not set #{f} and #{kf} at the same time."
257
+ end
258
+ end
259
+
260
+ def check_metric_configs
261
+ return unless @data_type == :metric
262
+
263
+ @metrics_from_event = false if @metric_name_key
264
+
265
+ return if @metrics_from_event
266
+
267
+ unless @metric_name_key
268
+ raise Fluent::ConfigError,
269
+ "`metric_name_key` is required when `metrics_from_event` is `false`."
270
+ end
271
+ return if @metric_value_key
272
+
273
+ raise Fluent::ConfigError,
274
+ "`metric_value_key` is required when `metric_name_key` is set."
275
+ end
276
+
277
+ def prepare_key_fields
278
+ KEY_FIELDS.each do |f|
279
+ v = instance_variable_get "@#{f}_key"
280
+ if v
281
+ attrs = v.split(".").freeze
282
+ if @keep_keys
283
+ instance_variable_set "@#{f}", ->(_, record) { attrs.inject(record) { |o, k| o[k] } }
284
+ else
285
+ instance_variable_set "@#{f}", lambda { |_, record|
286
+ attrs[0...-1].inject(record) { |o, k| o[k] }.delete(attrs[-1])
287
+ }
288
+ end
289
+ else
290
+ v = instance_variable_get "@#{f}"
291
+ next unless v
292
+
293
+ if v.include? TAG_PLACEHOLDER
294
+ instance_variable_set "@#{f}", ->(tag, _) { v.gsub(TAG_PLACEHOLDER, tag) }
295
+ else
296
+ instance_variable_set "@#{f}", ->(_, _) { v }
297
+ end
298
+ end
299
+ end
300
+ end
301
+
302
+ def configure_fields(conf)
303
+ # This loop looks dumb, but it is used to suppress the unused parameter configuration warning
304
+ conf.elements.select { |element| element.name == "fields" }.each do |element|
305
+ element.each_pair { |k, _v| element.key?(k) }
306
+ end
307
+
308
+ return unless @fields
309
+
310
+ @extra_fields = @fields.corresponding_config_element.to_h do |k, v|
311
+ [k, v.empty? ? k : v]
312
+ end
313
+ end
314
+
315
+ def pick_custom_format_method
316
+ if @data_type == :event
317
+ define_singleton_method :format, method(:format_event)
318
+ else
319
+ define_singleton_method :format, method(:format_metric)
320
+ end
321
+ end
322
+
323
+ def format_event(tag, time, record)
324
+ payload = {
325
+ host: @host ? @host.call(tag, record) : @default_host,
326
+ time: time.to_f.to_s
327
+ }.tap do |p|
328
+ if @time
329
+ time_value = @time.call(tag, record)
330
+ p[:time] = time_value unless time_value.nil?
331
+ end
332
+
333
+ p[:index] = @index.call(tag, record) if @index
334
+ p[:source] = @source.call(tag, record) if @source
335
+ p[:sourcetype] = @sourcetype.call(tag, record) if @sourcetype
336
+
337
+ # delete nil fields otherwise will get format error from HEC
338
+ %i[host index source sourcetype].each { |field| p.delete field if p[field].nil? }
339
+
340
+ if @extra_fields
341
+ p[:fields] = @extra_fields.transform_values { |field| record[field] }
342
+ p[:fields].delete_if { |_k, v| v.nil? }
343
+ # if a field is already in indexed fields, then remove it from the original event
344
+ @extra_fields.each_value { |field| record.delete field }
345
+ end
346
+
347
+ formatter = @formatters.find { |f| f.match? tag }
348
+ record = formatter.format(tag, time, record) if formatter
349
+
350
+ p[:event] = convert_to_utf8(record)
351
+ end
352
+
353
+ if payload[:event] == "{}"
354
+ log.warn { "Event after formatting was blank, not sending" }
355
+ return ""
356
+ end
357
+
358
+ Oj.dump(payload, mode: :compat)
359
+ end
360
+
361
+ def format_metric(tag, time, record)
362
+ payload = {
363
+ host: @host ? @host.call(tag, record) : @default_host,
364
+ time: time.to_f.to_s,
365
+ event: "metric"
366
+ }.tap do |p|
367
+ if @time
368
+ time_value = @time.call(tag, record)
369
+ p[:time] = time_value unless time_value.nil?
370
+ end
371
+ end
372
+
373
+ payload[:index] = @index.call(tag, record) if @index
374
+ payload[:source] = @source.call(tag, record) if @source
375
+ payload[:sourcetype] = @sourcetype.call(tag, record) if @sourcetype
376
+
377
+ unless @metrics_from_event
378
+ fields = {
379
+ metric_name: @metric_name.call(tag, record),
380
+ _value: @metric_value.call(tag, record)
381
+ }
382
+
383
+ if @extra_fields
384
+ fields.update(@extra_fields.transform_values { |field| record[field] })
385
+ fields.delete_if { |_k, v| v.nil? }
386
+ else
387
+ fields.update record
388
+ end
389
+
390
+ fields.delete_if { |_k, v| v.nil? }
391
+ payload[:fields] = convert_to_utf8(fields)
392
+
393
+ return Oj.dump(payload, mode: :compat)
394
+ end
395
+
396
+ # when metrics_from_event is true, generate one metric event for each key-value in record
397
+ payloads = record.map do |key, value|
398
+ { fields: { metric_name: key, _value: value } }.merge!(payload)
399
+ end
400
+
401
+ payloads.map { |p| Oj.dump(p, mode: :compat) }.join
402
+ end
403
+
404
+ def construct_api
405
+ if @full_url.empty?
406
+ URI("#{@protocol}://#{@hec_host}:#{@hec_port}/#{@hec_endpoint.delete_prefix("/")}")
407
+ else
408
+ URI("#{@full_url.delete_suffix("/")}/#{@hec_endpoint.delete_prefix("/")}")
409
+ end
410
+ rescue StandardError
411
+ if @full_url.empty?
412
+ raise Fluent::ConfigError, "hec_host (#{@hec_host}) and/or hec_port (#{@hec_port}) are invalid."
413
+ end
414
+
415
+ raise Fluent::ConfigError, "full_url (#{@full_url}) is invalid."
416
+ end
417
+
418
+ def write_to_splunk(chunk)
419
+ post = Net::HTTP::Post.new @api.request_uri
420
+ if @gzip_compression
421
+ post.add_field("Content-Encoding", "gzip")
422
+ gzip_stream = Zlib::GzipWriter.new StringIO.new
423
+ gzip_stream << chunk.read
424
+ post.body = gzip_stream.close.string
425
+ else
426
+ post.body = chunk.read
427
+ end
428
+
429
+ log.debug { "[Sending] Chunk: #{dump_unique_id_hex(chunk.unique_id)}(#{post.body.bytesize}B)." }
430
+ log.trace { "POST #{@api} body=#{post.body}" }
431
+
432
+ begin
433
+ t1 = Time.now
434
+ response = @conn.request @api, post
435
+ t2 = Time.now
436
+ rescue Net::HTTP::Persistent::Error => e
437
+ raise e.cause
438
+ end
439
+
440
+ process_response(response, post.body, t2 - t1)
441
+ end
442
+
443
+ def process_response(response, request_body, duration)
444
+ log.debug { "[Response] Status: #{response.code} Duration: #{duration}" }
445
+ log.trace { "[Response] POST #{@api}: #{response.inspect}" }
446
+
447
+ @metrics[:status_counter].increment(labels: metric_labels(status: response.code.to_s))
448
+
449
+ raise_err = response.code.to_s.start_with?("5") ||
450
+ (!@consume_chunk_on_4xx_errors && response.code.to_s.start_with?("4"))
451
+
452
+ # raise Exception to utilize Fluentd output plugin retry mechanism
453
+ raise "Server error (#{response.code}) for POST #{@api}, response: #{response.body}" if raise_err
454
+
455
+ # For both success response (2xx) we will consume the chunk.
456
+ return if response.code.to_s.start_with?("2")
457
+
458
+ log.error "#{self.class}: Failed POST to #{@api}, response: #{response.body}"
459
+ log.debug { "#{self.class}: Failed request body: #{request_body}" }
460
+ end
461
+
462
+ def convert_to_utf8(input)
463
+ if input.is_a?(Hash)
464
+ record = {}
465
+ input.each do |key, value|
466
+ record[convert_to_utf8(key)] = convert_to_utf8(value)
467
+ end
468
+ return record
469
+ end
470
+ return input.map { |value| convert_to_utf8(value) } if input.is_a?(Array)
471
+ return input unless input.respond_to?(:encode)
472
+
473
+ if @coerce_to_utf8
474
+ input.encode(
475
+ "utf-8",
476
+ invalid: :replace,
477
+ undef: :replace,
478
+ replace: @non_utf8_replacement_string
479
+ )
480
+ else
481
+ begin
482
+ input.encode("utf-8")
483
+ rescue EncodingError
484
+ log.error do
485
+ "Encountered encoding issues potentially due to non " \
486
+ "UTF-8 characters. To allow non-UTF-8 characters and " \
487
+ "replace them with spaces, please set \"coerce_to_utf8\" " \
488
+ "to true."
489
+ end
490
+ raise
491
+ end
492
+ end
493
+ end
494
+
495
+ def configure_metrics(conf)
496
+ @metric_labels = {
497
+ type: conf["@type"],
498
+ plugin_id: plugin_id
499
+ }
500
+
501
+ @metrics = {
502
+ record_counter: register_metric(::Prometheus::Client::Counter.new(
503
+ :splunk_output_write_records_count,
504
+ docstring: "The number of log records being sent",
505
+ labels: metric_label_keys
506
+ )),
507
+ bytes_counter: register_metric(::Prometheus::Client::Counter.new(
508
+ :splunk_output_write_bytes_count,
509
+ docstring: "The number of log bytes being sent",
510
+ labels: metric_label_keys
511
+ )),
512
+ status_counter: register_metric(::Prometheus::Client::Counter.new(
513
+ :splunk_output_write_status_count,
514
+ docstring: "The count of sends by response_code",
515
+ labels: metric_label_keys(status: "")
516
+ )),
517
+ write_bytes_histogram: register_metric(::Prometheus::Client::Histogram.new(
518
+ :splunk_output_write_payload_bytes,
519
+ docstring: "The size of the write payload in bytes",
520
+ buckets: [1024, 23_937, 47_875, 95_750, 191_500, 383_000, 766_000,
521
+ 1_149_000],
522
+ labels: metric_label_keys
523
+ )),
524
+ write_records_histogram: register_metric(::Prometheus::Client::Histogram.new(
525
+ :splunk_output_write_payload_records,
526
+ docstring: "The number of records written per write",
527
+ buckets: [1, 10, 25, 100, 200, 300, 500, 750, 1000, 1500],
528
+ labels: metric_label_keys
529
+ )),
530
+ write_latency_histogram: register_metric(::Prometheus::Client::Histogram.new(
531
+ :splunk_output_write_latency_seconds,
532
+ docstring: "The latency of writes",
533
+ labels: metric_label_keys
534
+ ))
535
+ }
536
+ end
537
+
538
+ def metric_labels(other_labels = {})
539
+ @metric_labels.merge other_labels
540
+ end
541
+
542
+ def metric_label_keys(other_labels = {})
543
+ (@metric_labels.merge other_labels).keys
544
+ end
545
+
546
+ def register_metric(metric)
547
+ if @registry.exist?(metric.name)
548
+ @registry.get(metric.name)
549
+ else
550
+ @registry.register(metric)
551
+ end
552
+ end
553
+ end
554
+ end
555
+ end
@@ -0,0 +1,49 @@
1
+ # frozen_string_literal: true
2
+
3
+ # Copyright 2018 Splunk Inc.
4
+ # Modifications Copyright 2025 G. Rahul Nutakki
5
+ #
6
+ # Licensed under the Apache License, Version 2.0 (the "License");
7
+ # you may not use this file except in compliance with the License.
8
+ # You may obtain a copy of the License at
9
+ #
10
+ # http://www.apache.org/licenses/LICENSE-2.0
11
+ #
12
+ # Unless required by applicable law or agreed to in writing, software
13
+ # distributed under the License is distributed on an "AS IS" BASIS,
14
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
15
+ # See the License for the specific language governing permissions and
16
+ # limitations under the License.
17
+
18
+ require "fluent/match"
19
+
20
+ module Fluent
21
+ module Plugin
22
+ module SplunkHecRadiant
23
+ # Helper class for pattern-based formatting
24
+ class MatchFormatter
25
+ def initialize(pattern, formatter)
26
+ # based on fluentd/lib/fluent/event_router.rb
27
+ patterns = pattern.split(/\s+/).map do |str|
28
+ Fluent::MatchPattern.create(str)
29
+ end
30
+ @pattern =
31
+ if patterns.length == 1
32
+ patterns[0]
33
+ else
34
+ Fluent::OrMatchPattern.new(patterns)
35
+ end
36
+ @formatter = formatter
37
+ end
38
+
39
+ def match?(tag)
40
+ @pattern.match tag
41
+ end
42
+
43
+ def format(tag, time, record)
44
+ @formatter.format tag, time, record
45
+ end
46
+ end
47
+ end
48
+ end
49
+ end
@@ -0,0 +1,9 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Fluent
4
+ module Plugin
5
+ module SplunkHecRadiant
6
+ VERSION = "0.1.0"
7
+ end
8
+ end
9
+ end
@@ -0,0 +1,4 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "fluent/plugin/out_splunk_hec_radiant"
4
+ require "fluent/plugin/splunk_hec_radiant/version"