├── .github └── workflows │ └── ruby.yml ├── .gitignore ├── .travis.yml ├── CHANGELOG.md ├── Gemfile ├── LICENSE ├── README.md ├── Rakefile ├── fluent-plugin-splunkhec.gemspec ├── lib └── fluent │ └── plugin │ └── out_splunkhec.rb └── test ├── helper.rb └── plugin └── test_out_splunkhec.rb /.github/workflows/ruby.yml: -------------------------------------------------------------------------------- 1 | # This workflow uses actions that are not certified by GitHub. 2 | # They are provided by a third-party and are governed by 3 | # separate terms of service, privacy policy, and support 4 | # documentation. 5 | # This workflow will download a prebuilt Ruby version, install dependencies and run tests with Rake 6 | # For more information see: https://github.com/marketplace/actions/setup-ruby-jruby-and-truffleruby 7 | 8 | name: Ruby 9 | 10 | on: 11 | push: 12 | branches: [ master ] 13 | pull_request: 14 | branches: [ master ] 15 | 16 | jobs: 17 | test: 18 | 19 | runs-on: ubuntu-latest 20 | strategy: 21 | matrix: 22 | ruby-version: ['2.6', '2.7', '3.0'] 23 | 24 | steps: 25 | - uses: actions/checkout@v2 26 | - name: Set up Ruby 27 | # To automatically get bug fixes and new Ruby versions for ruby/setup-ruby, 28 | # change this to (see https://github.com/ruby/setup-ruby#versioning): 29 | # uses: ruby/setup-ruby@v1 30 | uses: ruby/setup-ruby@473e4d8fe5dd94ee328fdfca9f8c9c7afc9dae5e 31 | with: 32 | ruby-version: ${{ matrix.ruby-version }} 33 | bundler-cache: true # runs 'bundle install' and caches installed gems automatically 34 | - name: Run tests 35 | run: bundle exec rake 36 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | 2 | # Created by https://www.gitignore.io/api/ruby,macos 3 | # Edit at https://www.gitignore.io/?templates=ruby,macos 4 | 5 | ### macOS ### 6 | # General 7 | .DS_Store 8 | .AppleDouble 9 | .LSOverride 10 | 11 | # Icon must end with two \r 12 | Icon 13 | 14 | # Thumbnails 15 | ._* 16 | 17 | # Files that might appear in the root of a volume 18 | .DocumentRevisions-V100 19 | .fseventsd 20 | .Spotlight-V100 21 | .TemporaryItems 22 | .Trashes 23 | .VolumeIcon.icns 24 | .com.apple.timemachine.donotpresent 25 | 26 | # Directories potentially created on remote AFP share 27 | .AppleDB 28 | .AppleDesktop 29 | Network Trash Folder 30 | Temporary Items 31 | .apdisk 32 | 33 | ### Ruby ### 34 | *.gem 35 | *.rbc 36 | /.config 37 | /coverage/ 38 | /InstalledFiles 39 | /pkg/ 40 | /spec/reports/ 41 | /spec/examples.txt 42 | /test/tmp/ 43 | /test/version_tmp/ 44 | /tmp/ 45 | 46 | # Used by dotenv library to load environment variables. 47 | # .env 48 | 49 | # Ignore Byebug command history file. 50 | .byebug_history 51 | 52 | ## Specific to RubyMotion: 53 | .dat* 54 | .repl_history 55 | build/ 56 | *.bridgesupport 57 | build-iPhoneOS/ 58 | build-iPhoneSimulator/ 59 | 60 | ## Specific to RubyMotion (use of CocoaPods): 61 | # 62 | # We recommend against adding the Pods directory to your .gitignore. However 63 | # you should judge for yourself, the pros and cons are mentioned at: 64 | # https://guides.cocoapods.org/using/using-cocoapods.html#should-i-check-the-pods-directory-into-source-control 65 | # vendor/Pods/ 66 | 67 | ## Documentation cache and generated files: 68 | /.yardoc/ 69 | /_yardoc/ 70 | /doc/ 71 | /rdoc/ 72 | 73 | ## Environment normalization: 74 | /.bundle/ 75 | /vendor/bundle 76 | /lib/bundler/man/ 77 | 78 | # for a library or gem, you might want to ignore these files since the code is 79 | # intended to run in multiple environments; otherwise, check them in: 80 | # Gemfile.lock 81 | # .ruby-version 82 | # .ruby-gemset 83 | 84 | # unless supporting rvm < 1.11.0 or doing something fancy, ignore this: 85 | .rvmrc 86 | 87 | # End of https://www.gitignore.io/api/ruby,macos 88 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: ruby 2 | rvm: 3 | - 2.4 4 | script: 5 | - "bundle exec rake" -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | ## 2.3 2 | 3 | Updated dependencies to solve security issues 4 | 5 | ## 2.2 6 | 7 | Replaced deprecated packet statement 8 | 9 | ## 2.1 10 | 11 | Replaced yajl/json_ge for regular yajl 12 | 13 | ## 2.0 14 | 15 | Migrate to use FluentD v1 API. It doesn't support backwards compatibility. 16 | 17 | ## 1.9 18 | 19 | Reverted source and sourcetpye settings. They now reflect the README. 20 | 21 | ## 1.8 22 | 23 | - Add expand function used in ES plugin an get variables from kubernetes tags in fluent.conf 24 | 25 | ## 1.7 26 | 27 | - Fixed HTTP request (removed verify none) 28 | - udpated testscript 29 | - implemented travis.yml 30 | 31 | ## 1.6 32 | 33 | - changed tag from sourcetype to source 34 | - improved ruby net HTTP implementation 35 | 36 | ## 1.5 37 | 38 | - changed yajl dependency to yajl-ruby 1.3.0 or above 39 | - fixed typo in readme 40 | 41 | ## 1.4 42 | 43 | Fixed undefined conversion error from ASCII-8BIT to UTF-8 by swapping json gem for yajl as used by fluentd 44 | 45 | ## 1.3 46 | 47 | In case of error in the connection exception is raised and fluent will retry. 48 | 49 | ## 1.2 50 | 51 | - Improved unit test coverage 52 | - Removed superfluous instance variables 53 | - Added feature send_batched_events 54 | - Rescue hosts that do not have hostname command installed. 55 | 56 | ## 1.1 57 | 58 | - Added send_event_as_json parameter to sent real json 59 | - Added usejson parameter to have the option to sent raw data with time included 60 | - Removed required from parameter definition 61 | 62 | ## 1.0.1 63 | 64 | Fixed config parameters used in Splunk URI. 65 | 66 | ## 1.0.0 67 | 68 | Added all Splunk HTTP Event Collector field options. 69 | 70 | ## 0.9.1 71 | 72 | Replaced RestClient for net/http. 73 | 74 | ## 0.9.0 75 | 76 | First version 77 | -------------------------------------------------------------------------------- /Gemfile: -------------------------------------------------------------------------------- 1 | source 'https://rubygems.org' 2 | 3 | # Specify your gem's dependencies in fluent-plugin-splunkhec.gemspec 4 | gemspec 5 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2022 Coen Meerbeek 2 | 3 | MIT License 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining 6 | a copy of this software and associated documentation files (the 7 | "Software"), to deal in the Software without restriction, including 8 | without limitation the rights to use, copy, modify, merge, publish, 9 | distribute, sublicense, and/or sell copies of the Software, and to 10 | permit persons to whom the Software is furnished to do so, subject to 11 | the following conditions: 12 | 13 | The above copyright notice and this permission notice shall be 14 | included in all copies or substantial portions of the Software. 15 | 16 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 17 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 18 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 19 | NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE 20 | LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 21 | OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION 22 | WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # fluent-plugin-splunkhec, a plugin for [Fluentd](http://fluentd.org) 2 | 3 | ## Overview 4 | 5 | ***Splunk HTTP Event Collector*** output plugin. 6 | 7 | Output data from any Fluent input plugin to the Splunk HTTP Event Collector (Splunk HEC). 8 | 9 | The Splunk HEC is running on a Heavy Forwarder or single instance. More info about the Splunk HEC architecture in a distributed environment can be found in the Splunk [Docs](http://dev.splunk.com/view/event-collector/SP-CAAAE73) 10 | 11 | ## Configuration 12 | 13 | ```config 14 | 15 | @type splunkhec 16 | host splunk.bluefactory.nl 17 | protocol https #optional 18 | port 8080 #optional 19 | token BAB747F3-744E-41BA 20 | index main #optional 21 | event_host fluentdhost #optional 22 | source fluentd #optional 23 | sourcetype data:type #optional 24 | usejson true #optional defaults to true 25 | send_event_as_json true #optional 26 | send_batched_events false #optional 27 | 28 | ``` 29 | 30 | ## config: host 31 | 32 | The host where the Splunk HEC is listening (Heavy Forwarder or Single Instance). 33 | 34 | ## config: protocol 35 | 36 | The protocol on which the Splunk HEC is listening. If you are going to use HTTPS make sure you use a signed certificate. Weak certificates are a work in progress. 37 | 38 | ## config: port 39 | 40 | The port on which the Splunk HEC is listening. 41 | 42 | ## config: token 43 | 44 | Every Splunk HEC requires a token to recieve data. You must configure this insite Splunk [Splunk HEC docs](http://docs.splunk.com/Documentation/Splunk/latest/Data/UsetheHTTPEventCollector). 45 | Put the token here. 46 | 47 | ## config: index 48 | 49 | The index on the Splunk side to store the data in. Please be aware that the Splunk HTTP Event Collector you've created has the permissions to write to this index. If you don't specify this the plug-in will use "main". 50 | 51 | ## config: event_host 52 | 53 | Specify the host-field for the event data in Splunk. If you don't specify this the plug-in will try to read the hostname running FluentD. 54 | 55 | ## config: source 56 | 57 | Specify the source-field for the event data in Splunk. If you don't specify this the plug-in will use "fluentd". 58 | 59 | ## config: sourcetype 60 | 61 | Specify the sourcetype-field for the event data in Splunk. If you don't specify this the plug-in will use the tag from the FluentD input plug-in. 62 | 63 | ## config: send_event_as_json 64 | 65 | Specify if an event should be sent as json rather than as a string. Can be 'true' or 'false'. If you don't specify then this will be 'false'. 66 | 67 | ## config: usejson 68 | 69 | Specify the event type as JSON (true|default) or raw (false) for sending Log4J messages so Splunk so it can parse the time field it self based on the format 'time' regex match found in the source, uses millisecond precision. 70 | 71 | ## config: send_batched_events 72 | 73 | Specify that all events in a FluentD chunk should be sent in batch to Splunk. Defaults to 'false' which sends one event at a time. Batching events will reduce the load on the Splunk HEC. Max chunk size is controlled by config parameter 'buffer_chunk_limit' and should be matched by the Splunk limit 'max_content_length'. Please see this [blog post](https://www.splunk.com/blog/2016/08/12/handling-http-event-collector-hec-content-length-too-large-errors-without-pulling-your-hair-out.html) for details. 74 | 75 | ## Contributing 76 | 77 | 1. Fork it 78 | 2. Create your feature branch (`git checkout -b my-new-feature`) 79 | 3. Commit your changes (`git commit -am 'Added some feature'`) 80 | 4. Push to the branch (`git push origin my-new-feature`) 81 | 5. Create new Pull Request 82 | 83 | ## TODO 84 | 85 | * Add support for SSL verification. 86 | 87 | ## Copyright 88 | 89 | Copyright (c) 2021 [LICENSE](LICENSE) for details. 90 | -------------------------------------------------------------------------------- /Rakefile: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env rake 2 | require "bundler/gem_tasks" 3 | 4 | require "rake/testtask" 5 | Rake::TestTask.new(:test) do |test| 6 | test.libs << 'lib' << 'test' 7 | test.pattern = 'test/**/test_*.rb' 8 | test.verbose = true 9 | end 10 | 11 | task :default => :test 12 | -------------------------------------------------------------------------------- /fluent-plugin-splunkhec.gemspec: -------------------------------------------------------------------------------- 1 | # -*- encoding: utf-8 -*- 2 | lib = File.expand_path('../lib', __FILE__) 3 | $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib) 4 | 5 | Gem::Specification.new do |gem| 6 | gem.name = "fluent-plugin-splunkhec" 7 | gem.version = "2.3" 8 | gem.authors = "Coen Meerbeek" 9 | gem.email = "cmeerbeek@gmail.com" 10 | gem.description = %q{Output plugin for the Splunk HTTP Event Collector.} 11 | gem.homepage = "https://github.com/cmeerbeek/fluent-plugin-splunkhec" 12 | gem.summary = %q{This plugin allows you to sent events to the Splunk HTTP Event Collector.} 13 | 14 | gem.files = `git ls-files`.split($\) 15 | gem.executables = gem.files.grep(%r{^bin/}).map{ |f| File.basename(f) } 16 | gem.test_files = gem.files.grep(%r{^(test|spec|features)/}) 17 | gem.require_paths = ["lib"] 18 | 19 | gem.add_dependency "fluentd", [">= 1.15.1", "< 2"] 20 | gem.add_dependency "yajl-ruby", '>= 1.4.3' 21 | gem.add_development_dependency "rake", '>= 13.0.6' 22 | gem.add_development_dependency "test-unit", '~> 3.5', '>= 3.5.3' 23 | gem.add_development_dependency "webmock", '>= 3.14.0' 24 | gem.license = 'MIT' 25 | end 26 | -------------------------------------------------------------------------------- /lib/fluent/plugin/out_splunkhec.rb: -------------------------------------------------------------------------------- 1 | require 'fluent/plugin/output' 2 | require 'net/http' 3 | require 'yajl' 4 | 5 | module Fluent::Plugin 6 | class SplunkHECOutput < Output 7 | Fluent::Plugin.register_output('splunkhec', self) 8 | 9 | helpers :compat_parameters, :event_emitter 10 | 11 | DEFAULT_BUFFER_TYPE = "memory" 12 | 13 | # Primary Splunk HEC configuration parameters 14 | config_param :host, :string, :default => 'localhost' 15 | config_param :protocol, :string, :default => 'http' 16 | config_param :port, :string, :default => '8088' 17 | config_param :token, :string 18 | 19 | # Splunk event parameters 20 | config_param :index, :string, :default => 'main' 21 | config_param :event_host, :string, :default => nil 22 | config_param :source, :string, :default => 'fluentd' 23 | config_param :sourcetype, :string, :default => 'tag' 24 | config_param :send_event_as_json, :bool, :default => false 25 | config_param :usejson, :bool, :default => true 26 | config_param :send_batched_events, :bool, :default => false 27 | 28 | config_section :buffer do 29 | config_set_default :@type, DEFAULT_BUFFER_TYPE 30 | end 31 | 32 | # This method is called before starting. 33 | # Here we construct the Splunk HEC URL to POST data to 34 | # If the configuration is invalid, raise Fluent::ConfigError. 35 | def configure(conf) 36 | compat_parameters_convert(conf, :buffer) 37 | super 38 | @splunk_url = @protocol + '://' + @host + ':' + @port + '/services/collector/event' 39 | log.info 'splunkhec: sending data to ' + @splunk_url 40 | 41 | if conf['event_host'] == nil 42 | begin 43 | @event_host = `hostname`.delete!("\n") 44 | rescue 45 | @event_host = 'unknown' 46 | end 47 | end 48 | @packer = Fluent::MessagePackFactory.engine_factory.packer 49 | end 50 | 51 | def start 52 | super 53 | end 54 | 55 | def shutdown 56 | super 57 | end 58 | 59 | def formatted_to_msgpack_binary? 60 | true 61 | end 62 | 63 | def multi_workers_ready? 64 | true 65 | end 66 | 67 | # This method is called when an event reaches to Fluentd. 68 | # Use msgpack to serialize the object. 69 | def format(tag, time, record) 70 | @packer.pack([tag, time, record]).to_s 71 | end 72 | 73 | def expand_param(param, tag, time, record) 74 | # check for '${ ... }' 75 | # yes => `eval` 76 | # no => return param 77 | return param if (param =~ /\${.+}/).nil? 78 | 79 | # check for 'tag_parts[]' 80 | # separated by a delimiter (default '.') 81 | tag_parts = tag.split(@delimiter) unless (param =~ /tag_parts\[.+\]/).nil? || tag.nil? 82 | 83 | # pull out section between ${} then eval 84 | inner = param.clone 85 | while inner.match(/\${.+}/) 86 | to_eval = inner.match(/\${(.+?)}/){$1} 87 | 88 | if !(to_eval =~ /record\[.+\]/).nil? && record.nil? 89 | return to_eval 90 | elsif !(to_eval =~/tag_parts\[.+\]/).nil? && tag_parts.nil? 91 | return to_eval 92 | elsif !(to_eval =~/time/).nil? && time.nil? 93 | return to_eval 94 | else 95 | inner.sub!(/\${.+?}/, eval( to_eval )) 96 | end 97 | end 98 | inner 99 | end 100 | 101 | # Loop through all records and sent them to Splunk 102 | def write(chunk) 103 | body = '' 104 | chunk.msgpack_each {|(tag,time,record)| 105 | 106 | # define index and sourcetype dynamically 107 | begin 108 | index = expand_param(@index, tag, time, record) 109 | sourcetype = expand_param(@sourcetype, tag, time, record) 110 | event_host = expand_param(@event_host, tag, time, record) 111 | token = expand_param(@token, tag, time, record) 112 | rescue => e 113 | # handle dynamic parameters misconfigurations 114 | router.emit_error_event(tag, time, record, e) 115 | next 116 | end 117 | log.debug "routing event from #{event_host} to #{index} index" 118 | log.debug "expanded token #{token}" 119 | 120 | # Parse record to Splunk event format 121 | case record 122 | when Integer 123 | event = record.to_s 124 | when Hash 125 | if @send_event_as_json 126 | event = Yajl::Encoder.encode(record) 127 | else 128 | event = Yajl::Encoder.encode(record).gsub("\"", %q(\\\")) 129 | end 130 | else 131 | event = record 132 | end 133 | 134 | sourcetype = @sourcetype == 'tag' ? tag : @sourcetype 135 | 136 | # Build body for the POST request 137 | if !@usejson 138 | event = record["time"]+ " " + Yajl::Encoder.encode(record["message"]).gsub(/^"|"$/,"") 139 | body << '{"time":"'+ DateTime.parse(record["time"]).strftime("%Q") +'", "event":"' + event + '", "sourcetype" :"' + sourcetype + '", "source" :"' + @source + '", "index" :"' + index + '", "host" : "' + event_host + '"}' 140 | elsif @send_event_as_json 141 | body << '{"time" :' + time.to_s + ', "event" :' + event + ', "sourcetype" :"' + sourcetype + '", "source" :"' + source + '", "index" :"' + index + '", "host" : "' + event_host + '"}' 142 | else 143 | body << '{"time" :' + time.to_s + ', "event" :"' + event + '", "sourcetype" :"' + sourcetype + '", "source" :"' + source + '", "index" :"' + index + '", "host" : "' + event_host + '"}' 144 | end 145 | 146 | if @send_batched_events 147 | body << "\n" 148 | else 149 | send_to_splunk(body, token) 150 | body = '' 151 | end 152 | } 153 | 154 | if @send_batched_events 155 | send_to_splunk(body, token) 156 | end 157 | end 158 | 159 | def send_to_splunk(body, token) 160 | log.debug "splunkhec: " + body + "\n" 161 | 162 | uri = URI(@splunk_url) 163 | 164 | # Create client 165 | http = Net::HTTP.new(uri.host, uri.port) 166 | http.set_debug_output(log.debug) 167 | 168 | # Create request 169 | req = Net::HTTP::Post.new(uri, "Content-Type" => "application/json; charset=utf-8", "Authorization" => "Splunk #{token}") 170 | req.body = body 171 | 172 | # Handle SSL 173 | if @protocol == 'https' 174 | http.use_ssl = true 175 | end 176 | 177 | # Send Request 178 | res = http.request(req) 179 | 180 | log.debug "splunkhec: HTTP Response Status Code is #{res.code}" 181 | 182 | if res.code.to_i != 200 183 | body = Yajl::Parser.parse(res.body) 184 | raise SplunkHECOutputError.new(body['text'], body['code'], body['invalid-event-number'], res.code) 185 | end 186 | end 187 | end 188 | 189 | class SplunkHECOutputError < StandardError 190 | def initialize(message, status_code, invalid_event_number, http_status_code) 191 | super("#{message} (http status code #{http_status_code}, status code #{status_code}, invalid event number #{invalid_event_number})") 192 | end 193 | end 194 | 195 | end 196 | -------------------------------------------------------------------------------- /test/helper.rb: -------------------------------------------------------------------------------- 1 | require 'rubygems' 2 | require 'bundler' 3 | require 'fluent/input' 4 | 5 | begin 6 | Bundler.setup(:default, :development) 7 | rescue Bundler::BundlerError => e 8 | $stderr.puts e.message 9 | $stderr.puts "Run `bundle install` to install missing gems" 10 | exit e.status_code 11 | end 12 | require "test/unit" 13 | 14 | $LOAD_PATH.unshift(File.join(File.dirname(__FILE__), "..", "lib")) 15 | $LOAD_PATH.unshift(File.dirname(__FILE__)) 16 | require "fluent/test" 17 | require "fluent/test/driver/output" 18 | 19 | require "fluent/plugin/out_splunkhec" 20 | 21 | class Test::Unit::TestCase 22 | end 23 | -------------------------------------------------------------------------------- /test/plugin/test_out_splunkhec.rb: -------------------------------------------------------------------------------- 1 | # coding: utf-8 2 | require 'helper' 3 | require 'webmock/test_unit' 4 | 5 | 6 | class SplunkHECOutputTest < Test::Unit::TestCase 7 | HOST = 'splunk.example.com' 8 | PROTOCOL = 'https' 9 | PORT = '8443' 10 | TOKEN = 'BAB747F3-744E-41BA' 11 | SOURCE = 'fluentd' 12 | INDEX = 'main' 13 | EVENT_HOST = 'some_host' 14 | SOURCETYPE = 'log' 15 | 16 | SPLUNK_URL = "#{PROTOCOL}://#{HOST}:#{PORT}/services/collector/event" 17 | 18 | ### for Splunk HEC 19 | CONFIG = %[ 20 | host #{HOST} 21 | protocol #{PROTOCOL} 22 | port #{PORT} 23 | token #{TOKEN} 24 | source #{SOURCE} 25 | index #{INDEX} 26 | event_host #{EVENT_HOST} 27 | ] 28 | 29 | def create_driver_splunkhec(conf = CONFIG) 30 | Fluent::Test::Driver::Output.new(Fluent::Plugin::SplunkHECOutput).configure(conf) 31 | end 32 | 33 | def setup 34 | Fluent::Test.setup 35 | require 'fluent/plugin/out_splunkhec' 36 | stub_request(:any, SPLUNK_URL) 37 | end 38 | 39 | def test_should_require_mandatory_parameter_token 40 | assert_raise Fluent::ConfigError do 41 | create_driver_splunkhec(%[]) 42 | end 43 | end 44 | 45 | def test_should_use_default_values_for_optional_parameters 46 | d = create_driver_splunkhec(%[token some_token]) 47 | assert_equal 'localhost', d.instance.host 48 | assert_equal 'http', d.instance.protocol 49 | assert_equal '8088', d.instance.port 50 | assert_equal 'main', d.instance.index 51 | assert_equal `hostname`.delete!("\n"), d.instance.event_host 52 | assert_equal 'tag', d.instance.sourcetype 53 | assert_equal 'fluentd', d.instance.source 54 | assert_equal false, d.instance.send_event_as_json 55 | assert_equal true, d.instance.usejson 56 | assert_equal false, d.instance.send_batched_events 57 | assert_equal 'some_token', d.instance.token 58 | end 59 | 60 | def test_should_configure_splunkhec 61 | d = create_driver_splunkhec 62 | assert_equal HOST, d.instance.host 63 | assert_equal PROTOCOL, d.instance.protocol 64 | assert_equal PORT, d.instance.port 65 | assert_equal TOKEN, d.instance.token 66 | end 67 | 68 | def test_should_post_formatted_event_to_splunk 69 | sourcetype = 'log' 70 | time = 123456 71 | record = {'message' => 'data'} 72 | 73 | splunk_request = stub_request(:post, SPLUNK_URL) 74 | .with( 75 | headers: { 76 | 'Authorization' => "Splunk #{TOKEN}", 77 | 'Content-Type' => 'application/json; charset=utf-8', 78 | }, 79 | body: { 80 | 'time' => time, 81 | 'event' => record.to_json, 82 | 'sourcetype' => sourcetype, 83 | 'source' => SOURCE, 84 | 'index' => INDEX, 85 | 'host' => EVENT_HOST 86 | }) 87 | 88 | d = create_driver_splunkhec(CONFIG + %[sourcetype #{sourcetype}]) 89 | d.run(default_tag: 'test') do 90 | d.feed(time, record) 91 | end 92 | 93 | assert_requested(splunk_request) 94 | end 95 | 96 | def test_should_use_tag_as_sourcetype_when_configured 97 | splunk_request = stub_request(:post, SPLUNK_URL).with(body: hash_including({'sourcetype' => 'test'})) 98 | 99 | d = create_driver_splunkhec(CONFIG + %[sourcetype test]) 100 | d.run(default_tag: 'test') do 101 | d.feed(123456, {'message' => 'data'}) 102 | end 103 | 104 | assert_requested(splunk_request) 105 | end 106 | 107 | def test_should_send_event_as_string_as_default 108 | record = {'message' => 'data'} 109 | splunk_request = stub_request(:post, SPLUNK_URL).with(body: hash_including({'event' => record.to_json})) 110 | 111 | d = create_driver_splunkhec(CONFIG + %[send_event_as_json false]) 112 | d.run(default_tag: 'test') do 113 | d.feed(record) 114 | end 115 | 116 | assert_requested(splunk_request) 117 | end 118 | 119 | def test_should_send_event_as_log4j_format_when_configured 120 | log_time = '2017-07-02 20:52:39' 121 | log_time_millis = '1499028759000' 122 | log_event = 'data' 123 | 124 | splunk_request = stub_request(:post, SPLUNK_URL) 125 | .with(body: hash_including({'time' => log_time_millis, 'event' => "#{log_time} #{log_event}"})) 126 | 127 | d = create_driver_splunkhec(CONFIG + %[usejson false]) 128 | d.run(default_tag: 'test') do 129 | d.feed({'time' => log_time, 'message' => log_event}) 130 | end 131 | 132 | assert_requested(splunk_request) 133 | end 134 | 135 | def test_should_send_event_as_json_when_configured 136 | record = {'message' => 'data'} 137 | 138 | splunk_request = stub_request(:post, SPLUNK_URL).with(body: hash_including({'event' => record})) 139 | 140 | d = create_driver_splunkhec(CONFIG + %[send_event_as_json true]) 141 | d.run(default_tag: 'test') do 142 | d.feed(record) 143 | end 144 | 145 | assert_requested(splunk_request) 146 | end 147 | 148 | def test_should_batch_post_all_events_in_chunk_when_configured 149 | record1 = {'message' => 'data'} 150 | record2 = {'message' => 'more data'} 151 | 152 | splunk_request = stub_request(:post, SPLUNK_URL).with(body: /\"event\" :#{record1.to_json}.*\"event\" :#{record2.to_json}/m) 153 | 154 | d = create_driver_splunkhec(CONFIG + %[ 155 | send_event_as_json true 156 | send_batched_events true]) 157 | 158 | d.run(default_tag: 'test') do 159 | d.feed(record1) 160 | d.feed(record2) 161 | end 162 | 163 | assert_requested(splunk_request) 164 | end 165 | 166 | def test_should_raise_exception_when_splunk_returns_error_to_make_fluentd_retry_later 167 | stub_request(:any, SPLUNK_URL).to_return(status: 403, body: {'text' => 'Token disabled', 'code' => 1}.to_json) 168 | 169 | assert_raise Fluent::Plugin::SplunkHECOutputError do 170 | d = create_driver_splunkhec 171 | d.run(default_tag: 'test') do 172 | d.feed({'message' => 'data'}) 173 | end 174 | end 175 | end 176 | 177 | def test_should_handle_ascii_8bit_encoded_message_with_utf8_chars_correctly 178 | record = {'message' => "\xC2\xA92017".b} 179 | 180 | splunk_request = stub_request(:post, SPLUNK_URL).with(body: hash_including({'event' => {'message' => '©2017'}})) 181 | 182 | d = create_driver_splunkhec(CONFIG + %[send_event_as_json true]) 183 | d.run(default_tag: 'test') do 184 | d.feed(record) 185 | end 186 | 187 | assert_requested(splunk_request) 188 | end 189 | 190 | end 191 | --------------------------------------------------------------------------------