fluent-plugin-splunk-hec 1.0.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,22 @@
1
+ require 'fluent/match'
2
+
3
+ class Fluent::Plugin::SplunkHecOutput::MatchFormatter
4
+ def initialize(pattern, formatter)
5
+ # stolen from fluentd/lib/fluent/event_router.rb
6
+ patterns = pattern.split(/\s+/).map { |str| Fluent::MatchPattern.create(str) }
7
+ @pattern = if patterns.length == 1
8
+ patterns[0]
9
+ else
10
+ Fluent::OrMatchPattern.new(patterns)
11
+ end
12
+ @formatter = formatter
13
+ end
14
+
15
+ def match?(tag)
16
+ @pattern.match tag
17
+ end
18
+
19
+ def format(tag, time, record)
20
+ @formatter.format tag, time, record
21
+ end
22
+ end
@@ -0,0 +1 @@
1
+ Fluent::Plugin::SplunkHecOutput::VERSION = File.read(File.expand_path('../../../../VERSION', File.dirname(__FILE__))).chomp.strip
@@ -0,0 +1,271 @@
1
+ require "test_helper"
2
+
3
+ describe Fluent::Plugin::SplunkHecOutput do
4
+ include Fluent::Test::Helpers
5
+ include PluginTestHelper
6
+
7
+ before { Fluent::Test.setup } # setup router and others
8
+
9
+ it { expect(::Fluent::Plugin::SplunkHecOutput::VERSION).wont_be_nil }
10
+
11
+ describe "hec_host validation" do
12
+ describe "invalid host" do
13
+ it "should require hec_host" do
14
+ expect{ create_output_driver }.must_raise Fluent::ConfigError
15
+ end
16
+
17
+ it { expect{ create_output_driver('hec_host %bad-host%') }.must_raise Fluent::ConfigError }
18
+ end
19
+
20
+ describe "good host" do
21
+ it {
22
+ expect(create_output_driver('hec_host splunk.com').instance.hec_host).must_equal "splunk.com"
23
+ }
24
+ end
25
+ end
26
+
27
+ it "should send request to Splunk" do
28
+ req = verify_sent_events { |batch|
29
+ expect(batch.size).must_equal 2
30
+ }
31
+ expect(req).must_be_requested times: 1
32
+ end
33
+
34
+ it "should use host machine's hostname for event host by default" do
35
+ verify_sent_events { |batch|
36
+ batch.each do |item|
37
+ expect(item['host']).must_equal Socket.gethostname
38
+ end
39
+ }
40
+ end
41
+
42
+ %w[index source sourcetype].each do |field|
43
+ it "should not set #{field} by default" do
44
+ verify_sent_events { |batch|
45
+ batch.each do |item|
46
+ expect(item).wont_include field
47
+ end
48
+ }
49
+ end
50
+ end
51
+
52
+ it "should support ${tag}" do
53
+ verify_sent_events(<<~CONF) { |batch|
54
+ index ${tag}
55
+ host ${tag}
56
+ source ${tag}
57
+ sourcetype ${tag}
58
+ CONF
59
+ batch.each do |item|
60
+ %w[index host source sourcetype].each { |field|
61
+ expect(%w[tag.event1 tag.event2]).must_include item[field]
62
+ }
63
+ end
64
+ }
65
+ end
66
+
67
+ it "should support *_key" do
68
+ verify_sent_events(<<~CONF) { |batch|
69
+ index_key level
70
+ host_key from
71
+ source_key file
72
+ sourcetype_key agent.name
73
+ CONF
74
+ batch.each { |item|
75
+ expect(item['index']).must_equal 'info'
76
+ expect(item['host']).must_equal 'my_machine'
77
+ expect(item['source']).must_equal 'cool.log'
78
+ expect(item['sourcetype']).must_equal 'test'
79
+
80
+ JSON.load(item['event']).tap do |event|
81
+ %w[level from file].each { |field| expect(event).wont_include field }
82
+ expect(event['agent']).wont_include 'name'
83
+ end
84
+ }
85
+ }
86
+ end
87
+
88
+ it "should remove nil fileds." do
89
+ verify_sent_events(<<~CONF) { |batch|
90
+ index_key nonexist
91
+ host_key nonexist
92
+ source_key nonexist
93
+ sourcetype_key nonexist
94
+ CONF
95
+ batch.each { |item|
96
+ expect(item).wont_be :has_key?, 'index'
97
+ expect(item).wont_be :has_key?, 'host'
98
+ expect(item).wont_be :has_key?, 'source'
99
+ expect(item).wont_be :has_key?, 'sourcetype'
100
+ }
101
+ }
102
+ end
103
+
104
+ describe 'formatter' do
105
+ it "should support replace the default json formater" do
106
+ verify_sent_events(<<~CONF) { |batch|
107
+ <format>
108
+ @type single_value
109
+ message_key log
110
+ add_newline false
111
+ </format>
112
+ CONF
113
+ batch.map { |item| item['event'] }
114
+ .each { |event| expect(event).must_equal "everything is good" }
115
+ }
116
+ end
117
+
118
+ it "should support multiple formatters" do
119
+ verify_sent_events(<<~CONF) { |batch|
120
+ source ${tag}
121
+ <format tag.event1>
122
+ @type single_value
123
+ message_key log
124
+ add_newline false
125
+ </format>
126
+ CONF
127
+ expect(batch.find { |item| item['source'] == 'tag.event1' }['event']).must_equal "everything is good"
128
+ expect(batch.find { |item| item['source'] == 'tag.event2' }['event']).must_be_instance_of Hash
129
+ }
130
+ end
131
+ end
132
+
133
+ it "should support fields for indexed field extraction" do
134
+ verify_sent_events(<<~CONF) { |batch|
135
+ <fields>
136
+ from
137
+ logLevel level
138
+ nonexist
139
+ </fields>
140
+ CONF
141
+ batch.each do |item|
142
+ JSON.load(item['event']).tap { |event|
143
+ expect(event).wont_include 'from'
144
+ expect(event).wont_include 'level'
145
+ }
146
+
147
+ expect(item['fields']['from']).must_equal 'my_machine'
148
+ expect(item['fields']['logLevel']).must_equal 'info'
149
+ expect(item['fields']).wont_be :has_key?, 'nonexist'
150
+ end
151
+ }
152
+ end
153
+
154
+ describe 'metric'do
155
+ it 'should check related configs' do
156
+ expect(
157
+ create_output_driver('hec_host somehost', 'data_type metric')
158
+ ).wont_be_nil
159
+
160
+ expect{
161
+ create_output_driver('hec_host somehost', 'data_type metric', 'metrics_from_event false')
162
+ }.must_raise Fluent::ConfigError
163
+
164
+ expect{
165
+ create_output_driver('hec_host somehost', 'data_type metric', 'metric_name_key x')
166
+ }.must_raise Fluent::ConfigError
167
+
168
+ expect(
169
+ create_output_driver('hec_host somehost', 'data_type metric', 'metric_name_key x', 'metric_value_key y')
170
+ ).wont_be_nil
171
+ end
172
+
173
+ it 'should have "metric" as event, and have proper fields' do
174
+ verify_sent_events(<<~CONF) { |batch|
175
+ data_type metric
176
+ metric_name_key from
177
+ metric_value_key value
178
+ CONF
179
+ batch.each do |item|
180
+ expect(item['event']).must_equal 'metric'
181
+ expect(item['fields']['metric_name']).must_equal 'my_machine'
182
+ expect(item['fields']['_value']).must_equal 100
183
+ expect(item['fields']['log']).must_equal 'everything is good'
184
+ expect(item['fields']['level']).must_equal 'info'
185
+ expect(item['fields']['file']).must_equal 'cool.log'
186
+ end
187
+ }
188
+ end
189
+
190
+ it 'should handle empty fields' do
191
+ verify_sent_events(<<~CONF) { |batch|
192
+ data_type metric
193
+ metric_name_key from
194
+ metric_value_key value
195
+ <fields>
196
+ </fields>
197
+ CONF
198
+ batch.each do |item|
199
+ # only "metric_name" and "_value"
200
+ expect(item['fields'].keys.size).must_equal 2
201
+ end
202
+ }
203
+ end
204
+
205
+ it 'should handle custom fields' do
206
+ verify_sent_events(<<~CONF) { |batch|
207
+ data_type metric
208
+ metric_name_key from
209
+ metric_value_key value
210
+ <fields>
211
+ level
212
+ filePath file
213
+ username
214
+ </fields>
215
+ CONF
216
+ batch.each do |item|
217
+ expect(item['fields'].keys.size).must_equal 4
218
+ expect(item['fields']['level']).must_equal 'info'
219
+ expect(item['fields']['filePath']).must_equal 'cool.log'
220
+ # null fields should be removed
221
+ expect(item['fields']).wont_be :has_key?, 'username'
222
+ end
223
+ }
224
+ end
225
+
226
+ it 'should treat each key-value in event as a metric' do
227
+ metrics = [
228
+ ['tag', event_time, {'cup': 0.5, 'memory': 100}],
229
+ ['tag', event_time, {'cup': 0.6, 'memory': 200}]
230
+ ]
231
+ with_stub_hec(events: metrics, conf: 'data_type metric') { |batch|
232
+ expect(batch.size).must_equal 4
233
+ }
234
+ end
235
+ end
236
+
237
+ def with_stub_hec(events:, conf: '', &blk)
238
+ host = "hec.splunk.com"
239
+ @driver = create_output_driver("hec_host #{host}", conf)
240
+
241
+ hec_req = stub_hec_request("https://#{host}:8088").with { |r|
242
+ blk.call r.body.split(/(?={)\s*(?<=})/).map { |item| JSON.load item }
243
+ }
244
+
245
+ @driver.run do
246
+ events.each { |evt| @driver.feed *evt }
247
+ end
248
+
249
+ hec_req
250
+ end
251
+
252
+ def verify_sent_events(conf = '', &blk)
253
+ event = {
254
+ "log" => "everything is good",
255
+ "level" => "info",
256
+ "from" => "my_machine",
257
+ "file" => "cool.log",
258
+ "value" => 100,
259
+ "agent" => {
260
+ "name" => "test",
261
+ "version" => "1.0.0"
262
+ }
263
+ }
264
+ events = [
265
+ ["tag.event1", event_time, {"id" => "1st"}.merge(Marshal.load(Marshal.dump(event)))],
266
+ ["tag.event2", event_time, {"id" => "2nd"}.merge(Marshal.load(Marshal.dump(event)))]
267
+ ]
268
+
269
+ with_stub_hec conf: conf, events: events, &blk
270
+ end
271
+ end
@@ -0,0 +1,39 @@
1
+ $LOAD_PATH.unshift File.expand_path("../../lib", __FILE__)
2
+ $LOAD_PATH.unshift File.expand_path("../lib", __FILE__)
3
+ require "fluent/plugin/out_splunk_hec"
4
+
5
+ require "fluent/test"
6
+ require "fluent/test/driver/output"
7
+ require "fluent/test/helpers"
8
+ require "minitest/autorun"
9
+ require "webmock/minitest"
10
+
11
+
12
+ # make assertions from webmock available in minitest/spec
13
+ module Minitest::Expectations
14
+ infect_an_assertion :assert_requested, :must_be_requested, :reverse
15
+ infect_an_assertion :assert_not_requested, :wont_be_requested, :reverse
16
+ end
17
+
18
+ TEST_HEC_TOKEN = "some-token".freeze
19
+
20
+ module PluginTestHelper
21
+ def fluentd_conf_for(*lines)
22
+ basic_config = [
23
+ "hec_token #{TEST_HEC_TOKEN}"
24
+ ]
25
+ (basic_config + lines).join("\n")
26
+ end
27
+
28
+ def create_output_driver(*configs)
29
+ Fluent::Test::Driver::Output.new(Fluent::Plugin::SplunkHecOutput).tap { |d|
30
+ d.configure(fluentd_conf_for(*configs))
31
+ }
32
+ end
33
+
34
+ def stub_hec_request(endpoint)
35
+ stub_request(:post, "#{endpoint}/services/collector").
36
+ with(headers: {"Authorization" => "Splunk #{TEST_HEC_TOKEN}", "User-Agent" => "fluent-plugin-splunk_hec_out/#{Fluent::Plugin::SplunkHecOutput::VERSION}"}).
37
+ to_return(body: '{"text":"Success","code":0}')
38
+ end
39
+ end
metadata ADDED
@@ -0,0 +1,188 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: fluent-plugin-splunk-hec
3
+ version: !ruby/object:Gem::Version
4
+ version: 1.0.0
5
+ platform: ruby
6
+ authors:
7
+ - Zhimin (Gimi) Liang
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain: []
11
+ date: 2018-05-15 00:00:00.000000000 Z
12
+ dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: fluentd
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - "~>"
18
+ - !ruby/object:Gem::Version
19
+ version: '1.0'
20
+ type: :runtime
21
+ prerelease: false
22
+ version_requirements: !ruby/object:Gem::Requirement
23
+ requirements:
24
+ - - "~>"
25
+ - !ruby/object:Gem::Version
26
+ version: '1.0'
27
+ - !ruby/object:Gem::Dependency
28
+ name: multi_json
29
+ requirement: !ruby/object:Gem::Requirement
30
+ requirements:
31
+ - - "~>"
32
+ - !ruby/object:Gem::Version
33
+ version: '1.13'
34
+ type: :runtime
35
+ prerelease: false
36
+ version_requirements: !ruby/object:Gem::Requirement
37
+ requirements:
38
+ - - "~>"
39
+ - !ruby/object:Gem::Version
40
+ version: '1.13'
41
+ - !ruby/object:Gem::Dependency
42
+ name: net-http-persistent
43
+ requirement: !ruby/object:Gem::Requirement
44
+ requirements:
45
+ - - "~>"
46
+ - !ruby/object:Gem::Version
47
+ version: '3.0'
48
+ type: :runtime
49
+ prerelease: false
50
+ version_requirements: !ruby/object:Gem::Requirement
51
+ requirements:
52
+ - - "~>"
53
+ - !ruby/object:Gem::Version
54
+ version: '3.0'
55
+ - !ruby/object:Gem::Dependency
56
+ name: bundler
57
+ requirement: !ruby/object:Gem::Requirement
58
+ requirements:
59
+ - - "~>"
60
+ - !ruby/object:Gem::Version
61
+ version: '1.16'
62
+ type: :development
63
+ prerelease: false
64
+ version_requirements: !ruby/object:Gem::Requirement
65
+ requirements:
66
+ - - "~>"
67
+ - !ruby/object:Gem::Version
68
+ version: '1.16'
69
+ - !ruby/object:Gem::Dependency
70
+ name: rake
71
+ requirement: !ruby/object:Gem::Requirement
72
+ requirements:
73
+ - - "~>"
74
+ - !ruby/object:Gem::Version
75
+ version: '10.0'
76
+ type: :development
77
+ prerelease: false
78
+ version_requirements: !ruby/object:Gem::Requirement
79
+ requirements:
80
+ - - "~>"
81
+ - !ruby/object:Gem::Version
82
+ version: '10.0'
83
+ - !ruby/object:Gem::Dependency
84
+ name: test-unit
85
+ requirement: !ruby/object:Gem::Requirement
86
+ requirements:
87
+ - - "~>"
88
+ - !ruby/object:Gem::Version
89
+ version: '3.0'
90
+ type: :development
91
+ prerelease: false
92
+ version_requirements: !ruby/object:Gem::Requirement
93
+ requirements:
94
+ - - "~>"
95
+ - !ruby/object:Gem::Version
96
+ version: '3.0'
97
+ - !ruby/object:Gem::Dependency
98
+ name: minitest
99
+ requirement: !ruby/object:Gem::Requirement
100
+ requirements:
101
+ - - "~>"
102
+ - !ruby/object:Gem::Version
103
+ version: '5.0'
104
+ type: :development
105
+ prerelease: false
106
+ version_requirements: !ruby/object:Gem::Requirement
107
+ requirements:
108
+ - - "~>"
109
+ - !ruby/object:Gem::Version
110
+ version: '5.0'
111
+ - !ruby/object:Gem::Dependency
112
+ name: webmock
113
+ requirement: !ruby/object:Gem::Requirement
114
+ requirements:
115
+ - - "~>"
116
+ - !ruby/object:Gem::Version
117
+ version: '3.2'
118
+ type: :development
119
+ prerelease: false
120
+ version_requirements: !ruby/object:Gem::Requirement
121
+ requirements:
122
+ - - "~>"
123
+ - !ruby/object:Gem::Version
124
+ version: '3.2'
125
+ description: A fluentd output plugin created by Splunk that writes events to splunk
126
+ indexers over HTTP Event Collector API.
127
+ email:
128
+ - zliang@splunk.com
129
+ executables: []
130
+ extensions: []
131
+ extra_rdoc_files: []
132
+ files:
133
+ - CODE_OF_CONDUCT.md
134
+ - Gemfile
135
+ - Gemfile.lock
136
+ - LICENSE
137
+ - README.md
138
+ - Rakefile
139
+ - VERSION
140
+ - fluent-plugin-splunk-hec.gemspec
141
+ - lib/fluent/plugin/out_splunk_hec.rb
142
+ - lib/fluent/plugin/out_splunk_hec/match_formatter.rb
143
+ - lib/fluent/plugin/out_splunk_hec/version.rb
144
+ - test/fluent/plugin/out_splunk_hec_test.rb
145
+ - test/lib/webmock/http_lib_adapters/curb_adapter.rb
146
+ - test/lib/webmock/http_lib_adapters/em_http_request_adapter.rb
147
+ - test/lib/webmock/http_lib_adapters/excon_adapter.rb
148
+ - test/lib/webmock/http_lib_adapters/http_rb_adapter.rb
149
+ - test/lib/webmock/http_lib_adapters/httpclient_adapter.rb
150
+ - test/lib/webmock/http_lib_adapters/manticore_adapter.rb
151
+ - test/lib/webmock/http_lib_adapters/patron_adapter.rb
152
+ - test/lib/webmock/http_lib_adapters/typhoeus_hydra_adapter.rb
153
+ - test/test_helper.rb
154
+ homepage: https://github.com/splunk/fluent-plugin-splunk-hec
155
+ licenses:
156
+ - Apache-2.0
157
+ metadata: {}
158
+ post_install_message:
159
+ rdoc_options: []
160
+ require_paths:
161
+ - lib
162
+ required_ruby_version: !ruby/object:Gem::Requirement
163
+ requirements:
164
+ - - ">="
165
+ - !ruby/object:Gem::Version
166
+ version: 2.4.0
167
+ required_rubygems_version: !ruby/object:Gem::Requirement
168
+ requirements:
169
+ - - ">="
170
+ - !ruby/object:Gem::Version
171
+ version: '0'
172
+ requirements: []
173
+ rubyforge_project:
174
+ rubygems_version: 2.7.6
175
+ signing_key:
176
+ specification_version: 4
177
+ summary: Fluentd plugin for Splunk HEC.
178
+ test_files:
179
+ - test/fluent/plugin/out_splunk_hec_test.rb
180
+ - test/lib/webmock/http_lib_adapters/patron_adapter.rb
181
+ - test/lib/webmock/http_lib_adapters/manticore_adapter.rb
182
+ - test/lib/webmock/http_lib_adapters/em_http_request_adapter.rb
183
+ - test/lib/webmock/http_lib_adapters/typhoeus_hydra_adapter.rb
184
+ - test/lib/webmock/http_lib_adapters/curb_adapter.rb
185
+ - test/lib/webmock/http_lib_adapters/httpclient_adapter.rb
186
+ - test/lib/webmock/http_lib_adapters/http_rb_adapter.rb
187
+ - test/lib/webmock/http_lib_adapters/excon_adapter.rb
188
+ - test/test_helper.rb