fluent-plugin-http-list 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,19 @@
1
+ *.gem
2
+ *.rbc
3
+ .bundle
4
+ .config
5
+ .yardoc
6
+ Gemfile.lock
7
+ InstalledFiles
8
+ _yardoc
9
+ coverage
10
+ doc/
11
+ lib/bundler/man
12
+ pkg
13
+ rdoc
14
+ spec/reports
15
+ test/tmp
16
+ test/version_tmp
17
+ tmp
18
+ test.conf
19
+ .idea
data/Gemfile ADDED
@@ -0,0 +1,3 @@
1
+ source 'https://rubygems.org'
2
+
3
+ gemspec
data/LICENSE ADDED
@@ -0,0 +1,20 @@
1
+ #
2
+ # Fluent HTTP List plugin
3
+ #
4
+ # Copyright 2013 M3, Inc.
5
+ # Written by Paul McCann (p-mccann@m3.com)
6
+ # Based on the HTTP Input Plugin by FURUHASHI Sadayuki included with Fluentd
7
+ #
8
+ # Licensed under the Apache License, Version 2.0 (the "License");
9
+ # you may not use this file except in compliance with the License.
10
+ # You may obtain a copy of the License at
11
+ #
12
+ # http://www.apache.org/licenses/LICENSE-2.0
13
+ #
14
+ # Unless required by applicable law or agreed to in writing, software
15
+ # distributed under the License is distributed on an "AS IS" BASIS,
16
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
17
+ # See the License for the specific language governing permissions and
18
+ # limitations under the License.
19
+ ##
20
+
@@ -0,0 +1,42 @@
1
+ # fluent-plugin-http-list
2
+
3
+ ## Overview
4
+
5
+ This plugin takes a JSON list of events as input via HTTP POST. If you're
6
+ sending a lot of events this simplifies your client's code and eliminates
7
+ the overhead of creating a lot of brief connections.
8
+
9
+ ※ Note that unlike the default HTTP plugin, this does *not* support msgpack.
10
+
11
+ ## Configuration
12
+
13
+ The HttpListInput plugin uses the same settings you would use for the standard
14
+ HTTP input plugin. Example:
15
+
16
+ <source>
17
+ type http_list
18
+ port 8888
19
+ bind 0.0.0.0
20
+ body_size_limit 32m
21
+ keepalive_timeout 10s
22
+ </source>
23
+
24
+ Like the HTTP input plugin, the tag is determined by the URL used, which means
25
+ all events in one request must have the same tag.
26
+
27
+ ## Usage
28
+
29
+ Have your logging system send JSON lists of events. Example:
30
+
31
+ curl -X POST -d 'json=[{"fish":"catfish","user":23},{"fish":"elephantfish","user":23}]' \
32
+ http://localhost:8888/fish.tracker
33
+
34
+ Each event will go to your output plugins as an individual event.
35
+
36
+ ## Copyright
37
+
38
+ Copyright (c) 2013 M3, Inc.
39
+
40
+ Based on the in_http plugin by FURUHASHI Sadayuki
41
+
42
+ Apache License, Version 2.0
@@ -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,18 @@
1
+ # -*- encoding: utf-8 -*-
2
+ Gem::Specification.new do |gem|
3
+ gem.name = "fluent-plugin-http-list"
4
+ gem.version = "0.1.0"
5
+ gem.authors = ["Paul McCann"]
6
+ gem.email = ["polm@dampfkraft.com"]
7
+ gem.description = %q{fluent plugin to accept multiple events in one HTTP request}
8
+ gem.summary = %q{fluent plugin to accept multiple events in one HTTP request}
9
+ gem.homepage = "https://github.com/m3dev/fluent-plugin-http-list"
10
+
11
+ gem.files = `git ls-files`.split($\)
12
+ gem.executables = gem.files.grep(%r{^bin/}).map{ |f| File.basename(f) }
13
+ gem.test_files = gem.files.grep(%r{^(test|spec|features)/})
14
+ gem.require_paths = ["lib"]
15
+
16
+ gem.add_development_dependency "fluentd"
17
+ gem.add_runtime_dependency "fluentd"
18
+ end
@@ -0,0 +1,269 @@
1
+ module Fluent
2
+
3
+ class HttpListInput < Input
4
+ Plugin.register_input('http_list', self)
5
+
6
+ include DetachMultiProcessMixin
7
+
8
+ require 'http/parser'
9
+
10
+ def initialize
11
+ require 'webrick/httputils'
12
+ super
13
+ end
14
+
15
+ config_param :port, :integer, :default => 9880
16
+ config_param :bind, :string, :default => '0.0.0.0'
17
+ config_param :body_size_limit, :size, :default => 32*1024*1024
18
+ config_param :keepalive_timeout, :time, :default => 10
19
+
20
+ def configure(conf)
21
+ super
22
+ end
23
+
24
+ class KeepaliveManager < Coolio::TimerWatcher
25
+ class TimerValue
26
+ def initialize
27
+ @value = 0
28
+ end
29
+ attr_accessor :value
30
+ end
31
+
32
+ def initialize(timeout)
33
+ super(1, true)
34
+ @cons = {}
35
+ @timeout = timeout.to_i
36
+ end
37
+
38
+ def add(sock)
39
+ @cons[sock] = sock
40
+ end
41
+
42
+ def delete(sock)
43
+ @cons.delete(sock)
44
+ end
45
+
46
+ def on_timer
47
+ @cons.each_pair {|sock,val|
48
+ if sock.step_idle > @timeout
49
+ sock.close
50
+ end
51
+ }
52
+ end
53
+ end
54
+
55
+ def start
56
+ $log.debug "listening for http on #{@bind}:#{@port}"
57
+ lsock = TCPServer.new(@bind, @port)
58
+
59
+ detach_multi_process do
60
+ super
61
+ @km = KeepaliveManager.new(@keepalive_timeout)
62
+ @lsock = Coolio::TCPServer.new(lsock, nil, Handler, @km, method(:on_request), @body_size_limit)
63
+
64
+ @loop = Coolio::Loop.new
65
+ @loop.attach(@km)
66
+ @loop.attach(@lsock)
67
+
68
+ @thread = Thread.new(&method(:run))
69
+ end
70
+ end
71
+
72
+ def shutdown
73
+ @loop.watchers.each {|w| w.detach }
74
+ @loop.stop
75
+ @lsock.close
76
+ @thread.join
77
+ end
78
+
79
+ def run
80
+ @loop.run
81
+ rescue
82
+ $log.error "unexpected error", :error=>$!.to_s
83
+ $log.error_backtrace
84
+ end
85
+
86
+ def on_request(path_info, params)
87
+ begin
88
+ path = path_info[1..-1] # remove /
89
+ tag = path.split('/').join('.')
90
+
91
+ if js = params['json']
92
+ records = JSON.parse(js)
93
+ p records
94
+ else
95
+ raise "'json' parameter is required" + params.keys.to_s
96
+ end
97
+
98
+ time = params['time'].nil? ? Engine.now : params['time'].to_i
99
+
100
+ rescue
101
+ return ["400 Bad Request", {'Content-type'=>'text/plain'}, "400 Bad Request\n#{$!}\n"]
102
+ end
103
+
104
+
105
+ begin
106
+ records.each{|r|
107
+ Engine.emit(tag, time, r)
108
+ }
109
+ rescue
110
+ return ["500 Internal Server Error", {'Content-type'=>'text/plain'}, "500 Internal Server Error\n#{$!}\n"]
111
+ end
112
+
113
+ return ["200 OK", {'Content-type'=>'text/plain'}, ""]
114
+ end
115
+
116
+ class Handler < Coolio::Socket
117
+ def initialize(io, km, callback, body_size_limit)
118
+ super(io)
119
+ @km = km
120
+ @callback = callback
121
+ @body_size_limit = body_size_limit
122
+ @content_type = ""
123
+ @next_close = false
124
+
125
+ @idle = 0
126
+ @km.add(self)
127
+ end
128
+
129
+ def step_idle
130
+ @idle += 1
131
+ end
132
+
133
+ def on_close
134
+ @km.delete(self)
135
+ end
136
+
137
+ def on_connect
138
+ @parser = Http::Parser.new(self)
139
+ end
140
+
141
+ def on_read(data)
142
+ @idle = 0
143
+ @parser << data
144
+ rescue
145
+ $log.warn "unexpected error", :error=>$!.to_s
146
+ $log.warn_backtrace
147
+ close
148
+ end
149
+
150
+ def on_message_begin
151
+ @body = ''
152
+ end
153
+
154
+ def on_headers_complete(headers)
155
+ expect = nil
156
+ size = nil
157
+ if @parser.http_version == [1, 1]
158
+ #Modified to always be false - Paul McCann 2012/10/24
159
+ #Changed because it was likely cause of slowness in production
160
+ @keep_alive = false
161
+ else
162
+ @keep_alive = false
163
+ end
164
+ headers.each_pair {|k,v|
165
+ case k
166
+ when /Expect/i
167
+ expect = v
168
+ when /Content-Length/i
169
+ size = v.to_i
170
+ when /Content-Type/i
171
+ @content_type = v
172
+ when /Connection/i
173
+ if v =~ /close/i
174
+ @keep_alive = false
175
+ elsif v =~ /Keep-alive/i
176
+ @keep_alive = true
177
+ end
178
+ end
179
+ }
180
+ if expect
181
+ if expect == '100-continue'
182
+ if !size || size < @body_size_limit
183
+ send_response_nobody("100 Continue", {})
184
+ else
185
+ send_response_and_close("413 Request Entity Too Large", {}, "Too large")
186
+ end
187
+ else
188
+ send_response_and_close("417 Expectation Failed", {}, "")
189
+ end
190
+ end
191
+ end
192
+
193
+ def on_body(chunk)
194
+ if @body.bytesize + chunk.bytesize > @body_size_limit
195
+ unless closing?
196
+ send_response_and_close("413 Request Entity Too Large", {}, "Too large")
197
+ end
198
+ return
199
+ end
200
+ @body << chunk
201
+ end
202
+
203
+ def on_message_complete
204
+ return if closing?
205
+
206
+ params = WEBrick::HTTPUtils.parse_query(@parser.query_string)
207
+
208
+ if @content_type =~ /^application\/x-www-form-urlencoded/
209
+ params.update WEBrick::HTTPUtils.parse_query(@body)
210
+ elsif @content_type =~ /^multipart\/form-data; boundary=(.+)/
211
+ boundary = WEBrick::HTTPUtils.dequote($1)
212
+ params.update WEBrick::HTTPUtils.parse_form_data(@body, boundary)
213
+ elsif @content_type =~ /^application\/json/
214
+ params['json'] = @body
215
+ end
216
+ path_info = @parser.request_path
217
+
218
+ code, header, body = *@callback.call(path_info, params)
219
+ body = body.to_s
220
+
221
+ if @keep_alive
222
+ header['Connection'] = 'Keep-Alive'
223
+ send_response(code, header, body)
224
+ else
225
+ send_response_and_close(code, header, body)
226
+ end
227
+ end
228
+
229
+ def on_write_complete
230
+ close if @next_close
231
+ end
232
+
233
+ def send_response_and_close(code, header, body)
234
+ send_response(code, header, body)
235
+ @next_close = true
236
+ end
237
+
238
+ def closing?
239
+ @next_close
240
+ end
241
+
242
+ def send_response(code, header, body)
243
+ header['Content-length'] ||= body.bytesize
244
+ header['Content-type'] ||= 'text/plain'
245
+
246
+ data = %[HTTP/1.1 #{code}\r\n]
247
+ header.each_pair {|k,v|
248
+ data << "#{k}: #{v}\r\n"
249
+ }
250
+ data << "\r\n"
251
+ write data
252
+
253
+ write body
254
+ end
255
+
256
+ def send_response_nobody(code, header)
257
+ data = %[HTTP/1.1 #{code}\r\n]
258
+ header.each_pair {|k,v|
259
+ data << "#{k}: #{v}\r\n"
260
+ }
261
+ data << "\r\n"
262
+ write data
263
+ end
264
+ end
265
+ end
266
+
267
+
268
+ end
269
+
@@ -0,0 +1,156 @@
1
+ require 'fluent/test'
2
+ require 'net/http'
3
+
4
+ class HttpListInputTest < Test::Unit::TestCase
5
+ def setup
6
+ Fluent::Test.setup
7
+ require 'fluent/plugin/in_http_list'
8
+ end
9
+
10
+ CONFIG = %[
11
+ port 9911
12
+ bind 127.0.0.1
13
+ body_size_limit 10m
14
+ keepalive_timeout 5
15
+ ]
16
+
17
+ def create_driver(conf=CONFIG)
18
+ Fluent::Test::InputTestDriver.new(Fluent::HttpListInput).configure(conf)
19
+ end
20
+
21
+ def test_configure
22
+ d = create_driver
23
+ assert_equal 9911, d.instance.port
24
+ assert_equal '127.0.0.1', d.instance.bind
25
+ assert_equal 10*1024*1024, d.instance.body_size_limit
26
+ assert_equal 5, d.instance.keepalive_timeout
27
+ end
28
+
29
+ #
30
+ # sending events as a post parameter named 'json'
31
+ # e.g. curl -XPOST 'http://127.0.0.1:9911/tag1' -d'time=1357860203&json=[{"a":1}]'
32
+ #
33
+ def test_parameter
34
+ d = create_driver
35
+
36
+ time = Time.parse("2013-01-10 23:23:23 UTC").to_i
37
+
38
+ d.expect_emit "tag1", time, {"a"=>1}
39
+ d.expect_emit "tag2", time, {"a"=>2}
40
+
41
+ d.run do
42
+ d.expected_emits.each {|tag,time,record|
43
+ res = post_as_parameter("/#{tag}", time.to_s, [record].to_json)
44
+ assert_equal "200", res.code
45
+ }
46
+ end
47
+ end
48
+
49
+ #
50
+ # sending evens as a request body (application/json)
51
+ # e.g. curl -XPOST 'http://127.0.0.1:9911/tag1?time=1357860203' -d'[{"a":1}]' -H 'Content-Type:application/json; charset=utf-8'
52
+ #
53
+ def test_application_json
54
+ d = create_driver
55
+
56
+ time = Time.parse("2013-01-10 23:23:23 UTC").to_i
57
+
58
+ d.expect_emit "tag1", time, {"a"=>1}
59
+ d.expect_emit "tag2", time, {"a"=>2}
60
+
61
+ d.run do
62
+ d.expected_emits.each {|tag,time,record|
63
+ res = post_as_application_json("/#{tag}", time.to_s, [record].to_json)
64
+ assert_equal "200", res.code
65
+ }
66
+ end
67
+ end
68
+
69
+ def test_multiple_events_as_parameter
70
+ d = create_driver
71
+
72
+ time = Time.parse("2013-01-10 23:23:23 UTC").to_i
73
+
74
+ d.expect_emit "tag1", time, {"a"=>1}
75
+ d.expect_emit "tag1", time, {"a"=>2}
76
+ d.expect_emit "tag1", time, {"a"=>3}
77
+ d.expect_emit "tag2", time, {"a"=>4}
78
+ d.expect_emit "tag2", time, {"a"=>5}
79
+ d.expect_emit "tag2", time, {"a"=>6}
80
+
81
+ test_events = [
82
+ ["tag1", (1..3).to_a.collect {|x| {"a"=>x}}],
83
+ ["tag2", (4..6).to_a.collect {|x| {"a"=>x}}]
84
+ ]
85
+
86
+ d.run do
87
+ test_events.each {|tag, events|
88
+ res = post_as_parameter("/#{tag}", time, events.to_json)
89
+ assert_equal "200", res.code
90
+ }
91
+ end
92
+ end
93
+
94
+ def test_multiple_events_as_application_json
95
+ d = create_driver
96
+
97
+ time = Time.parse("2013-01-10 23:23:23 UTC").to_i
98
+
99
+ d.expect_emit "tag1", time, {"a"=>1}
100
+ d.expect_emit "tag1", time, {"a"=>2}
101
+ d.expect_emit "tag1", time, {"a"=>3}
102
+ d.expect_emit "tag2", time, {"a"=>4}
103
+ d.expect_emit "tag2", time, {"a"=>5}
104
+ d.expect_emit "tag2", time, {"a"=>6}
105
+
106
+ test_events = [
107
+ ["tag1", (1..3).to_a.collect {|x| {"a"=>x}}],
108
+ ["tag2", (4..6).to_a.collect {|x| {"a"=>x}}]
109
+ ]
110
+
111
+ d.run do
112
+ test_events.each {|tag, events|
113
+ res = post_as_application_json("/#{tag}", time.to_s, events.to_json)
114
+ assert_equal "200", res.code
115
+ }
116
+ end
117
+ end
118
+
119
+ def test_zero_events_as_parameter
120
+ # An empty event list should not fail
121
+ d = create_driver
122
+ time = Time.parse("2013-01-10 23:23:23 UTC").to_i
123
+ d.run do
124
+ res = post_as_parameter("/zero", time.to_s, [].to_json)
125
+ assert_equal "200", res.code
126
+ end
127
+ end
128
+
129
+ def test_zero_events_as_application_json
130
+ # An empty event list should not fail
131
+ d = create_driver
132
+ time = Time.parse("2013-01-10 23:23:23 UTC").to_i
133
+ d.run do
134
+ res = post_as_application_json("/zero", time.to_s, [].to_json)
135
+ assert_equal "200", res.code
136
+ end
137
+ end
138
+
139
+ private
140
+
141
+ def post_as_parameter(path, time, json)
142
+ http = Net::HTTP.new("127.0.0.1", 9911)
143
+ req = Net::HTTP::Post.new(path, {})
144
+ req.set_form_data({"time" => time, "json" => json})
145
+ http.request(req)
146
+ end
147
+
148
+ def post_as_application_json(path, time, json)
149
+ http = Net::HTTP.new("127.0.0.1", 9911)
150
+ req = Net::HTTP::Post.new("#{path}?time=#{time.to_s}", {"content-type"=>"application/json; charset=utf-8"})
151
+ req.body = json
152
+ http.request(req)
153
+ end
154
+
155
+ end
156
+
metadata ADDED
@@ -0,0 +1,86 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: fluent-plugin-http-list
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.1.0
5
+ prerelease:
6
+ platform: ruby
7
+ authors:
8
+ - Paul McCann
9
+ autorequire:
10
+ bindir: bin
11
+ cert_chain: []
12
+ date: 2013-01-16 00:00:00.000000000 Z
13
+ dependencies:
14
+ - !ruby/object:Gem::Dependency
15
+ name: fluentd
16
+ requirement: !ruby/object:Gem::Requirement
17
+ none: false
18
+ requirements:
19
+ - - ! '>='
20
+ - !ruby/object:Gem::Version
21
+ version: '0'
22
+ type: :development
23
+ prerelease: false
24
+ version_requirements: !ruby/object:Gem::Requirement
25
+ none: false
26
+ requirements:
27
+ - - ! '>='
28
+ - !ruby/object:Gem::Version
29
+ version: '0'
30
+ - !ruby/object:Gem::Dependency
31
+ name: fluentd
32
+ requirement: !ruby/object:Gem::Requirement
33
+ none: false
34
+ requirements:
35
+ - - ! '>='
36
+ - !ruby/object:Gem::Version
37
+ version: '0'
38
+ type: :runtime
39
+ prerelease: false
40
+ version_requirements: !ruby/object:Gem::Requirement
41
+ none: false
42
+ requirements:
43
+ - - ! '>='
44
+ - !ruby/object:Gem::Version
45
+ version: '0'
46
+ description: fluent plugin to accept multiple events in one HTTP request
47
+ email:
48
+ - polm@dampfkraft.com
49
+ executables: []
50
+ extensions: []
51
+ extra_rdoc_files: []
52
+ files:
53
+ - .gitignore
54
+ - Gemfile
55
+ - LICENSE
56
+ - README.md
57
+ - Rakefile
58
+ - fluent-plugin-http-list.gemspec
59
+ - lib/fluent/plugin/in_http_list.rb
60
+ - test/plugin/test_in_http_list.rb
61
+ homepage: https://github.com/m3dev/fluent-plugin-http-list
62
+ licenses: []
63
+ post_install_message:
64
+ rdoc_options: []
65
+ require_paths:
66
+ - lib
67
+ required_ruby_version: !ruby/object:Gem::Requirement
68
+ none: false
69
+ requirements:
70
+ - - ! '>='
71
+ - !ruby/object:Gem::Version
72
+ version: '0'
73
+ required_rubygems_version: !ruby/object:Gem::Requirement
74
+ none: false
75
+ requirements:
76
+ - - ! '>='
77
+ - !ruby/object:Gem::Version
78
+ version: '0'
79
+ requirements: []
80
+ rubyforge_project:
81
+ rubygems_version: 1.8.24
82
+ signing_key:
83
+ specification_version: 3
84
+ summary: fluent plugin to accept multiple events in one HTTP request
85
+ test_files:
86
+ - test/plugin/test_in_http_list.rb