fluent-plugin-out-http 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.
data/.gitignore ADDED
@@ -0,0 +1,7 @@
1
+ *~
2
+ \#*
3
+ .\#*
4
+ *.gem
5
+ .bundle
6
+ Gemfile.lock
7
+ vendor
data/Gemfile ADDED
@@ -0,0 +1,4 @@
1
+ source 'https://rubygems.org'
2
+
3
+ # Specify your gem's dependencies in fluent-plugin-out-http.gemspec
4
+ gemspec
data/LICENSE.txt ADDED
@@ -0,0 +1,11 @@
1
+ Licensed under the Apache License, Version 2.0 (the "License");
2
+ you may not use this file except in compliance with the License.
3
+ You may obtain a copy of the License at
4
+
5
+ http://www.apache.org/licenses/LICENSE-2.0
6
+
7
+ Unless required by applicable law or agreed to in writing, software
8
+ distributed under the License is distributed on an "AS IS" BASIS,
9
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
10
+ See the License for the specific language governing permissions and
11
+ limitations under the License.
data/README.md ADDED
@@ -0,0 +1,23 @@
1
+ # fluent-plugin-out-http
2
+
3
+ A generic [fluentd][1] output plugin for sending logs to an HTTP endpoint.
4
+
5
+ ## Configs
6
+
7
+ <match *>
8
+ type http
9
+ endpoint_url http://localhost.local/api/
10
+ http_method put
11
+ serializer json
12
+ rate_limit_msec 100
13
+ authentication basic
14
+ username alice
15
+ password bobpop
16
+ </match>
17
+
18
+ ----
19
+
20
+ Heavily based on [fluent-plugin-growthforecast][2]
21
+
22
+ [1]: http://fluentd.org/
23
+ [2]: https://github.com/tagomoris/fluent-plugin-growthforecast
data/Rakefile ADDED
@@ -0,0 +1,11 @@
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
@@ -0,0 +1,21 @@
1
+ # -*- encoding: utf-8 -*-
2
+
3
+ Gem::Specification.new do |gem|
4
+ gem.name = "fluent-plugin-out-http"
5
+ gem.version = "0.1.0"
6
+ gem.authors = ["Marica Odagaki"]
7
+ gem.email = ["ento.entotto@gmail.com"]
8
+ gem.summary = %q{A generic Fluentd output plugin to send logs to an HTTP endpoint}
9
+ gem.description = gem.summary
10
+ gem.homepage = "https://github.com/ento/fluent-plugin-out-http"
11
+
12
+ gem.files = `git ls-files`.split($\)
13
+ gem.executables = gem.files.grep(%r{^bin/}).map{ |f| File.basename(f) }
14
+ gem.test_files = gem.files.grep(%r{^(test|spec|features)/})
15
+ gem.require_paths = ["lib"]
16
+
17
+ gem.add_development_dependency "bundler"
18
+ gem.add_development_dependency "json"
19
+ gem.add_development_dependency "fluentd"
20
+ gem.add_runtime_dependency "fluentd"
21
+ end
@@ -0,0 +1,127 @@
1
+ require 'json'
2
+
3
+ class Fluent::HTTPOutput < Fluent::Output
4
+ Fluent::Plugin.register_output('http', self)
5
+
6
+ def initialize
7
+ super
8
+ require 'net/http'
9
+ require 'uri'
10
+ end
11
+
12
+ # Endpoint URL ex. localhost.local/api/
13
+ config_param :endpoint_url, :string
14
+
15
+ # HTTP method
16
+ config_param :http_method, :string, :default => :post
17
+
18
+ # form | json
19
+ config_param :serializer, :string, :default => :form
20
+
21
+ # Simple rate limiting: ignore any records within `rate_limit_msec`
22
+ # since the last one.
23
+ config_param :rate_limit_msec, :integer, :default => 0
24
+
25
+ # nil | 'none' | 'basic'
26
+ config_param :authentication, :string, :default => nil
27
+ config_param :username, :string, :default => ''
28
+ config_param :password, :string, :default => ''
29
+
30
+ def configure(conf)
31
+ super
32
+
33
+ serializers = [:json, :form]
34
+ @serializer = if serializers.include? @serializer.intern
35
+ @serializer.intern
36
+ else
37
+ :form
38
+ end
39
+
40
+ http_methods = [:get, :put, :post, :delete]
41
+ @http_method = if http_methods.include? @http_method.intern
42
+ @http_method.intern
43
+ else
44
+ :post
45
+ end
46
+
47
+ @auth = case @authentication
48
+ when 'basic' then :basic
49
+ else
50
+ :none
51
+ end
52
+ end
53
+
54
+ def start
55
+ super
56
+ end
57
+
58
+ def shutdown
59
+ super
60
+ end
61
+
62
+ def format_url(tag, time, record)
63
+ @endpoint_url
64
+ end
65
+
66
+ def set_body(req, tag, time, record)
67
+ if @serializer == :json
68
+ set_json_body(req, record)
69
+ else
70
+ req.set_form_data(record)
71
+ end
72
+ req
73
+ end
74
+
75
+ def set_header(req, tag, time, record)
76
+ req
77
+ end
78
+
79
+ def set_json_body(req, data)
80
+ req.body = JSON.dump(data)
81
+ req['Content-Type'] = 'application/json'
82
+ end
83
+
84
+ def create_request(tag, time, record)
85
+ url = format_url(tag, time, record)
86
+ uri = URI.parse(url)
87
+ req = Net::HTTP.const_get(@http_method.to_s.capitalize).new(uri.path)
88
+ set_body(req, tag, time, record)
89
+ set_header(req, tag, time, record)
90
+ return req, uri
91
+ end
92
+
93
+ def send_request(req, uri)
94
+ is_rate_limited = (@rate_limit_msec != 0 and not @last_request_time.nil?)
95
+ if is_rate_limited and ((Time.now.to_f - @last_request_time) * 1000.0 < @rate_limit_msec)
96
+ $log.info('Dropped request due to rate limiting')
97
+ return
98
+ end
99
+
100
+ res = nil
101
+ begin
102
+ if @auth and @auth == :basic
103
+ req.basic_auth(@username, @password)
104
+ end
105
+ @last_request_time = Time.now.to_f
106
+ res = Net::HTTP.new(uri.host, uri.port).start {|http| http.request(req) }
107
+ rescue IOError, EOFError, SystemCallError
108
+ # server didn't respond
109
+ $log.warn "Net::HTTP.#{req.method.capitalize} raises exception: #{$!.class}, '#{$!.message}'"
110
+ end
111
+ unless res and res.is_a?(Net::HTTPSuccess)
112
+ $log.warn "failed to #{req.method} #{uri} (#{res.code} #{res.message} #{res.body})"
113
+ end
114
+ end
115
+
116
+ def handle_record(tag, time, record)
117
+ req, uri = create_request(tag, time, record)
118
+ send_request(req, uri)
119
+ end
120
+
121
+ def emit(tag, es, chain)
122
+ es.each do |time, record|
123
+ handle_record(tag, time, record)
124
+ end
125
+ chain.next
126
+ end
127
+ end
@@ -0,0 +1,54 @@
1
+ require 'rubygems'
2
+ require 'bundler'
3
+
4
+ begin
5
+ Bundler.setup(:default, :development)
6
+ rescue Bundler::BundlerError => e
7
+ $stderr.puts e.message
8
+ $stderr.puts "Run `bundle install` to install missing gems"
9
+ exit e.status_code
10
+ end
11
+
12
+ require 'test/unit'
13
+
14
+ $LOAD_PATH.unshift(File.join(File.dirname(__FILE__), '..', 'lib'))
15
+ $LOAD_PATH.unshift(File.dirname(__FILE__))
16
+
17
+ require 'fluent/test'
18
+
19
+ unless ENV.has_key?('VERBOSE')
20
+ nulllogger = Object.new
21
+ nulllogger.instance_eval {|obj|
22
+ def method_missing(method, *args)
23
+ # pass
24
+ end
25
+ }
26
+ $log = nulllogger
27
+ end
28
+
29
+ class Test::Unit::TestCase
30
+ end
31
+
32
+ require 'webrick'
33
+
34
+ # to handle POST/PUT/DELETE ...
35
+ module WEBrick::HTTPServlet
36
+ class ProcHandler < AbstractServlet
37
+ alias do_POST do_GET
38
+ alias do_PUT do_GET
39
+ alias do_DELETE do_GET
40
+ end
41
+ end
42
+
43
+ def get_code(server, port, path, headers={})
44
+ require 'net/http'
45
+ Net::HTTP.start(server, port){|http|
46
+ http.get(path, headers).code
47
+ }
48
+ end
49
+ def get_content(server, port, path, headers={})
50
+ require 'net/http'
51
+ Net::HTTP.start(server, port){|http|
52
+ http.get(path, headers).body
53
+ }
54
+ end
@@ -0,0 +1,281 @@
1
+ require 'json'
2
+ require 'fluent/test/http_output_test'
3
+ require 'fluent/plugin/out_http'
4
+
5
+
6
+ TEST_LISTEN_PORT = 5126
7
+
8
+
9
+ class HTTPOutputTestBase < Test::Unit::TestCase
10
+ # setup / teardown for servers
11
+ def setup
12
+ Fluent::Test.setup
13
+ @posts = []
14
+ @puts = []
15
+ @prohibited = 0
16
+ @auth = false
17
+ @dummy_server_thread = Thread.new do
18
+ srv = if ENV['VERBOSE']
19
+ WEBrick::HTTPServer.new({:BindAddress => '127.0.0.1', :Port => TEST_LISTEN_PORT})
20
+ else
21
+ logger = WEBrick::Log.new('/dev/null', WEBrick::BasicLog::DEBUG)
22
+ WEBrick::HTTPServer.new({:BindAddress => '127.0.0.1', :Port => TEST_LISTEN_PORT, :Logger => logger, :AccessLog => []})
23
+ end
24
+ begin
25
+ allowed_methods = %w(POST PUT)
26
+ srv.mount_proc('/api/') { |req,res|
27
+ unless allowed_methods.include? req.request_method
28
+ res.status = 405
29
+ res.body = 'request method mismatch'
30
+ next
31
+ end
32
+ if @auth and req.header['authorization'][0] == 'Basic YWxpY2U6c2VjcmV0IQ==' # pattern of user='alice' passwd='secret!'
33
+ # ok, authorized
34
+ elsif @auth
35
+ res.status = 403
36
+ @prohibited += 1
37
+ next
38
+ else
39
+ # ok, authorization not required
40
+ end
41
+
42
+ record = {:auth => nil}
43
+ if req.content_type == 'application/json'
44
+ record[:json] = JSON.parse(req.body)
45
+ else
46
+ record[:form] = Hash[*(req.body.split('&').map{|kv|kv.split('=')}.flatten)]
47
+ end
48
+
49
+ instance_variable_get("@#{req.request_method.downcase}s").push(record)
50
+
51
+ res.status = 200
52
+ }
53
+ srv.mount_proc('/') { |req,res|
54
+ res.status = 200
55
+ res.body = 'running'
56
+ }
57
+ srv.start
58
+ ensure
59
+ srv.shutdown
60
+ end
61
+ end
62
+
63
+ # to wait completion of dummy server.start()
64
+ require 'thread'
65
+ cv = ConditionVariable.new
66
+ watcher = Thread.new {
67
+ connected = false
68
+ while not connected
69
+ begin
70
+ get_content('localhost', TEST_LISTEN_PORT, '/')
71
+ connected = true
72
+ rescue Errno::ECONNREFUSED
73
+ sleep 0.1
74
+ rescue StandardError => e
75
+ p e
76
+ sleep 0.1
77
+ end
78
+ end
79
+ cv.signal
80
+ }
81
+ mutex = Mutex.new
82
+ mutex.synchronize {
83
+ cv.wait(mutex)
84
+ }
85
+ end
86
+
87
+ def test_dummy_server
88
+ host = '127.0.0.1'
89
+ port = TEST_LISTEN_PORT
90
+ client = Net::HTTP.start(host, port)
91
+
92
+ assert_equal '200', client.request_get('/').code
93
+ assert_equal '200', client.request_post('/api/service/metrics/hoge', 'number=1&mode=gauge').code
94
+
95
+ assert_equal 1, @posts.size
96
+
97
+ assert_equal '1', @posts[0][:form]['number']
98
+ assert_equal 'gauge', @posts[0][:form]['mode']
99
+ assert_nil @posts[0][:auth]
100
+
101
+ @auth = true
102
+
103
+ assert_equal '403', client.request_post('/api/service/metrics/pos', 'number=30&mode=gauge').code
104
+
105
+ req_with_auth = lambda do |number, mode, user, pass|
106
+ url = URI.parse("http://#{host}:#{port}/api/service/metrics/pos")
107
+ req = Net::HTTP::Post.new(url.path)
108
+ req.basic_auth user, pass
109
+ req.set_form_data({'number'=>number, 'mode'=>mode})
110
+ req
111
+ end
112
+
113
+ assert_equal '403', client.request(req_with_auth.call(500, 'count', 'alice', 'wrong password!')).code
114
+
115
+ assert_equal '403', client.request(req_with_auth.call(500, 'count', 'alice', 'wrong password!')).code
116
+
117
+ assert_equal 1, @posts.size
118
+
119
+ assert_equal '200', client.request(req_with_auth.call(500, 'count', 'alice', 'secret!')).code
120
+
121
+ assert_equal 2, @posts.size
122
+
123
+ end
124
+
125
+ def teardown
126
+ @dummy_server_thread.kill
127
+ @dummy_server_thread.join
128
+ end
129
+ end
130
+
131
+ class HTTPOutputTest < HTTPOutputTestBase
132
+ CONFIG = %[
133
+ endpoint_url http://127.0.0.1:#{TEST_LISTEN_PORT}/api/
134
+ ]
135
+
136
+ CONFIG_JSON = %[
137
+ endpoint_url http://127.0.0.1:#{TEST_LISTEN_PORT}/api/
138
+ serializer json
139
+ ]
140
+
141
+ CONFIG_PUT = %[
142
+ endpoint_url http://127.0.0.1:#{TEST_LISTEN_PORT}/api/
143
+ http_method put
144
+ ]
145
+
146
+ RATE_LIMIT_MSEC = 1200
147
+
148
+ CONFIG_RATE_LIMIT = %[
149
+ endpoint_url http://127.0.0.1:#{TEST_LISTEN_PORT}/api/
150
+ rate_limit_msec #{RATE_LIMIT_MSEC}
151
+ ]
152
+
153
+ def create_driver(conf=CONFIG, tag='test.metrics')
154
+ Fluent::Test::OutputTestDriver.new(Fluent::HTTPOutput, tag).configure(conf)
155
+ end
156
+
157
+ def test_configure
158
+ d = create_driver
159
+ assert_equal "http://127.0.0.1:#{TEST_LISTEN_PORT}/api/", d.instance.endpoint_url
160
+ assert_equal :form, d.instance.serializer
161
+
162
+ d = create_driver CONFIG_JSON
163
+ assert_equal "http://127.0.0.1:#{TEST_LISTEN_PORT}/api/", d.instance.endpoint_url
164
+ assert_equal :json, d.instance.serializer
165
+ end
166
+
167
+ def test_emit_form
168
+ d = create_driver
169
+ d.emit({ 'field1' => 50, 'field2' => 20, 'field3' => 10, 'otherfield' => 1 })
170
+ d.run
171
+
172
+ assert_equal 1, @posts.size
173
+ record = @posts[0]
174
+
175
+ assert_equal '50', record[:form]['field1']
176
+ assert_equal '20', record[:form]['field2']
177
+ assert_equal '10', record[:form]['field3']
178
+ assert_equal '1', record[:form]['otherfield']
179
+ assert_nil record[:auth]
180
+
181
+ d.emit({ 'field1' => 50, 'field2' => 20, 'field3' => 10, 'otherfield' => 1 })
182
+ d.run
183
+
184
+ assert_equal 2, @posts.size
185
+ end
186
+
187
+ def test_emit_form_put
188
+ d = create_driver CONFIG_PUT
189
+ d.emit({ 'field1' => 50 })
190
+ d.run
191
+
192
+ assert_equal 0, @posts.size
193
+ assert_equal 1, @puts.size
194
+ record = @puts[0]
195
+
196
+ assert_equal '50', record[:form]['field1']
197
+ assert_nil record[:auth]
198
+
199
+ d.emit({ 'field1' => 50 })
200
+ d.run
201
+
202
+ assert_equal 0, @posts.size
203
+ assert_equal 2, @puts.size
204
+ end
205
+
206
+ def test_emit_json
207
+ d = create_driver CONFIG_JSON
208
+ d.emit({ 'field1' => 50, 'field2' => 20, 'field3' => 10, 'otherfield' => 1 })
209
+ d.run
210
+
211
+ assert_equal 1, @posts.size
212
+ record = @posts[0]
213
+
214
+ assert_equal 50, record[:json]['field1']
215
+ assert_equal 20, record[:json]['field2']
216
+ assert_equal 10, record[:json]['field3']
217
+ assert_equal 1, record[:json]['otherfield']
218
+ assert_nil record[:auth]
219
+ end
220
+
221
+ def test_rate_limiting
222
+ d = create_driver CONFIG_RATE_LIMIT
223
+ record = { :k => 1 }
224
+
225
+ last_emit = _current_msec
226
+ d.emit(record)
227
+ d.run
228
+
229
+ assert_equal 1, @posts.size
230
+
231
+ d.emit({})
232
+ d.run
233
+ assert last_emit + RATE_LIMIT_MSEC > _current_msec, "Still under rate limiting interval"
234
+ assert_equal 1, @posts.size
235
+
236
+ sleep (last_emit + RATE_LIMIT_MSEC - _current_msec) * 0.001
237
+
238
+ assert last_emit + RATE_LIMIT_MSEC < _current_msec, "No longer under rate limiting interval"
239
+ d.emit(record)
240
+ d.run
241
+ assert_equal 2, @posts.size
242
+ end
243
+
244
+ def _current_msec
245
+ Time.now.to_f * 1000
246
+ end
247
+
248
+ def test_auth
249
+ @auth = true # enable authentication of dummy server
250
+
251
+ d = create_driver(CONFIG, 'test.metrics')
252
+ d.emit({ 'field1' => 50, 'field2' => 20, 'field3' => 10, 'otherfield' => 1 })
253
+ d.run # failed in background, and output warn log
254
+
255
+ assert_equal 0, @posts.size
256
+ assert_equal 1, @prohibited
257
+
258
+ d = create_driver(CONFIG + %[
259
+ authentication basic
260
+ username alice
261
+ password wrong_password
262
+ ], 'test.metrics')
263
+ d.emit({ 'field1' => 50, 'field2' => 20, 'field3' => 10, 'otherfield' => 1 })
264
+ d.run # failed in background, and output warn log
265
+
266
+ assert_equal 0, @posts.size
267
+ assert_equal 2, @prohibited
268
+
269
+ d = create_driver(CONFIG + %[
270
+ authentication basic
271
+ username alice
272
+ password secret!
273
+ ], 'test.metrics')
274
+ d.emit({ 'field1' => 50, 'field2' => 20, 'field3' => 10, 'otherfield' => 1 })
275
+ d.run # failed in background, and output warn log
276
+
277
+ assert_equal 1, @posts.size
278
+ assert_equal 2, @prohibited
279
+ end
280
+
281
+ end
metadata ADDED
@@ -0,0 +1,101 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: fluent-plugin-out-http
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.1.0
5
+ prerelease:
6
+ platform: ruby
7
+ authors:
8
+ - Marica Odagaki
9
+ autorequire:
10
+ bindir: bin
11
+ cert_chain: []
12
+ date: 2013-02-14 00:00:00.000000000 +09:00
13
+ default_executable:
14
+ dependencies:
15
+ - !ruby/object:Gem::Dependency
16
+ name: bundler
17
+ requirement: &70205234747520 !ruby/object:Gem::Requirement
18
+ none: false
19
+ requirements:
20
+ - - ! '>='
21
+ - !ruby/object:Gem::Version
22
+ version: '0'
23
+ type: :development
24
+ prerelease: false
25
+ version_requirements: *70205234747520
26
+ - !ruby/object:Gem::Dependency
27
+ name: json
28
+ requirement: &70205234747080 !ruby/object:Gem::Requirement
29
+ none: false
30
+ requirements:
31
+ - - ! '>='
32
+ - !ruby/object:Gem::Version
33
+ version: '0'
34
+ type: :development
35
+ prerelease: false
36
+ version_requirements: *70205234747080
37
+ - !ruby/object:Gem::Dependency
38
+ name: fluentd
39
+ requirement: &70205234746660 !ruby/object:Gem::Requirement
40
+ none: false
41
+ requirements:
42
+ - - ! '>='
43
+ - !ruby/object:Gem::Version
44
+ version: '0'
45
+ type: :development
46
+ prerelease: false
47
+ version_requirements: *70205234746660
48
+ - !ruby/object:Gem::Dependency
49
+ name: fluentd
50
+ requirement: &70205234746240 !ruby/object:Gem::Requirement
51
+ none: false
52
+ requirements:
53
+ - - ! '>='
54
+ - !ruby/object:Gem::Version
55
+ version: '0'
56
+ type: :runtime
57
+ prerelease: false
58
+ version_requirements: *70205234746240
59
+ description: A generic Fluentd output plugin to send logs to an HTTP endpoint
60
+ email:
61
+ - ento.entotto@gmail.com
62
+ executables: []
63
+ extensions: []
64
+ extra_rdoc_files: []
65
+ files:
66
+ - .gitignore
67
+ - Gemfile
68
+ - LICENSE.txt
69
+ - README.md
70
+ - Rakefile
71
+ - fluent-plugin-out-http.gemspec
72
+ - lib/fluent/plugin/out_http.rb
73
+ - lib/fluent/test/http_output_test.rb
74
+ - test/plugin/test_out_http.rb
75
+ has_rdoc: true
76
+ homepage: https://github.com/ento/fluent-plugin-out-http
77
+ licenses: []
78
+ post_install_message:
79
+ rdoc_options: []
80
+ require_paths:
81
+ - lib
82
+ required_ruby_version: !ruby/object:Gem::Requirement
83
+ none: false
84
+ requirements:
85
+ - - ! '>='
86
+ - !ruby/object:Gem::Version
87
+ version: '0'
88
+ required_rubygems_version: !ruby/object:Gem::Requirement
89
+ none: false
90
+ requirements:
91
+ - - ! '>='
92
+ - !ruby/object:Gem::Version
93
+ version: '0'
94
+ requirements: []
95
+ rubyforge_project:
96
+ rubygems_version: 1.6.2
97
+ signing_key:
98
+ specification_version: 3
99
+ summary: A generic Fluentd output plugin to send logs to an HTTP endpoint
100
+ test_files:
101
+ - test/plugin/test_out_http.rb