fluent-plugin-splunk-hec 1.0.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -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