fluent-plugin-http-ex 0.0.1

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.
checksums.yaml ADDED
@@ -0,0 +1,15 @@
1
+ ---
2
+ !binary "U0hBMQ==":
3
+ metadata.gz: !binary |-
4
+ MGZlNmQ3NTVmNDkyNDRjNTMyYjYzMWFkNzJjYzBjZTdkYzNlZDI1Yw==
5
+ data.tar.gz: !binary |-
6
+ NTFkNjIyN2MyNDgwNmY2ZmRkMDNhMDAxYjZlNmU0ZmRmMTQ0ZmY4Ng==
7
+ !binary "U0hBNTEy":
8
+ metadata.gz: !binary |-
9
+ MjFiNWVjNDI2YTYzNjNkZjZlY2Y5ZDJjMGQ5YzMyYjQ1ZmRjNTExZjBkYmNj
10
+ MGRmMDlhNmY3ZDc4ZjE5OTk1ZWVlZTg4MGM2MzBlNjNhZDMyOTQwNGFkZjVj
11
+ NTM5MWFkM2RiYjY5YzEyZTE0MDM2YzZjZWY5YzJiNDlkMTEyN2E=
12
+ data.tar.gz: !binary |-
13
+ MDUwZGEwN2YxZjcyMmMxYmUxYzk0YmJmNzRmOTk2ZDVlY2RiMDNlNjNmMGQz
14
+ NDlhZGRiNDNkMGJjZWRhNDlmNjZkMzg4YzMxNTVlNjQxNWY5MDg4ZjRmZTkx
15
+ NWUyNjJkODU1NmYyMGZhZGE3ZjIzNThkNGI1M2Q1NGMyMjM2NGI=
data/.gitignore ADDED
@@ -0,0 +1,17 @@
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
data/Gemfile ADDED
@@ -0,0 +1,7 @@
1
+ source 'https://rubygems.org'
2
+
3
+ gem "fluentd"
4
+
5
+ group :test do
6
+ gem "nio4r"
7
+ end
data/LICENSE.txt ADDED
@@ -0,0 +1,13 @@
1
+ Copyright (C) 2013 hiro-su
2
+
3
+ Licensed under the Apache License, Version 2.0 (the "License");
4
+ you may not use this file except in compliance with the License.
5
+ You may obtain a copy of the License at
6
+
7
+ http://www.apache.org/licenses/LICENSE-2.0
8
+
9
+ Unless required by applicable law or agreed to in writing, software
10
+ distributed under the License is distributed on an "AS IS" BASIS,
11
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12
+ See the License for the specific language governing permissions and
13
+ limitations under the License.
data/README.md ADDED
@@ -0,0 +1,279 @@
1
+ # Fluent::Plugin::Http::Ex
2
+
3
+ ## Overview
4
+
5
+ This plugin takes JSON or MessagePack of events as input via a single, list or chunked
6
+ HTTP POST request and emits each as an individual event to your output plugins.
7
+ If you're sending a lot of events this simplifies your client's code and eliminates
8
+ the overhead of creating a lot of brief connections.
9
+
10
+ ## Configuration
11
+
12
+ The ExHttpInput plugin uses the same settings you would use for the standard
13
+ HTTP input plugin. Example:
14
+
15
+ <source>
16
+ type http_ex
17
+ port 8888
18
+ bind 0.0.0.0
19
+ body_size_limit 32m
20
+ keepalive_timeout 300s #0s is not timeout
21
+ </source>
22
+
23
+ Like the HTTP input plugin, the tag is determined by the URL used, which means
24
+ all events in one request must have the same tag.
25
+
26
+ ## Usage
27
+
28
+ Have your logging system send JSON or Message of events. Example:
29
+
30
+ Base URL
31
+
32
+ http://localhost:8888
33
+
34
+ ### json
35
+ #### case 1
36
+
37
+ resource
38
+
39
+ j or null
40
+
41
+ header
42
+
43
+ Content-type: application/x-www-form-urlencoded
44
+
45
+ body
46
+
47
+ json=<json data>
48
+
49
+ sample
50
+
51
+ $ curl -X POST -d 'json={"action":"login","user":2}' \
52
+ http://localhost:8888/j/test.tag.here;
53
+
54
+ $ curl -X POST -d 'json={"action":"login","user":2}' \
55
+ http://localhost:8888/test.tag.here;
56
+
57
+ #### case 2
58
+
59
+ resource
60
+
61
+ j or null
62
+
63
+ header
64
+
65
+ Content-type: application/json
66
+
67
+ body
68
+
69
+ <json data>
70
+
71
+ sample
72
+
73
+ $ curl -X POST -H 'Content-Type: application/json' -d '{"action":"login","user":2}' \
74
+ http://localhost:8888/j/test.tag.here;
75
+
76
+ $ curl -X POST -H 'Content-Type: application/json' -d '{"action":"login","user":2}' \
77
+ http://localhost:8888/test.tag.here;
78
+
79
+ ### json list
80
+ #### case 1
81
+
82
+ resource
83
+
84
+ js
85
+
86
+ header
87
+
88
+ Content-type: application/x-www-form-urlencoded
89
+
90
+ body
91
+
92
+ json=<json list data>
93
+
94
+ sample
95
+
96
+ $ curl -X POST -d 'json=[{"action":"login","user":2},{"action":"login","user":2}]' \
97
+ http://localhost:8888/js/test.tag.here;
98
+
99
+ #### case 2
100
+
101
+ resource
102
+
103
+ js
104
+
105
+ header
106
+
107
+ Content-type: application/json
108
+
109
+ body
110
+
111
+ json=<json list data>
112
+
113
+ sample
114
+
115
+ $ curl -X POST -d '[{"action":"login","user":2},{"action":"login","user":2}]' \
116
+ http://localhost:8888/js/test.tag.here;
117
+
118
+ ### msgpack
119
+ #### case 1
120
+
121
+ resource
122
+
123
+ m or null
124
+
125
+ header
126
+
127
+ Content-type: application/x-www-form-urlencoded
128
+
129
+ body
130
+
131
+ msgpack=<hash msgpack data>
132
+ hash.to_msgpack
133
+
134
+ #### case2
135
+
136
+ resource
137
+
138
+ m or null
139
+
140
+ header
141
+
142
+ Content-type: application/x-msgpack
143
+
144
+ body
145
+
146
+ <msgpack data>
147
+ hash.to_msgpack
148
+
149
+ ### msgpack list
150
+ #### case 1
151
+
152
+ resource
153
+
154
+ ms
155
+
156
+ header
157
+
158
+ Content-type: application/x-www-form-urlencoded
159
+
160
+ body
161
+
162
+ msgpack=<msgpack list data>
163
+ [hash,hash,hash].to_msgpack
164
+
165
+ #### case 2
166
+
167
+ resource
168
+
169
+ ms
170
+
171
+ header
172
+
173
+ Content-type: application/x-msgpack
174
+
175
+ body
176
+
177
+ msgpack=<msgpack list data>
178
+ [hash,hash,hash].to_msgpack
179
+
180
+ ### msgpack chunked
181
+
182
+ resource
183
+
184
+ ms
185
+
186
+ header
187
+
188
+ Content-type: application/x-msgpack
189
+ Transfer-Encoding: chunked
190
+
191
+ body
192
+
193
+ <msgpack chunk data>
194
+ "#{hash.to_msgpack}#{hash.to_msgpack}"...
195
+
196
+
197
+ Each event in the list will be sent to your output plugins as an individual
198
+ event.
199
+
200
+ ## Performance
201
+
202
+ Comparison of in_http and in_http_ex.
203
+ send 10,000 messages.
204
+
205
+
206
+ machine spec
207
+
208
+ Mac OS X 10.8.2
209
+ 1.8 GHz Intel Core i5
210
+ 8 GB 1600 MHz DDR3
211
+
212
+ ### in_http
213
+
214
+ json
215
+
216
+ $ time ruby examples/json.rb
217
+
218
+ real 2m27.480s
219
+ user 0m7.252s
220
+ sys 0m4.438s
221
+
222
+ msgpack
223
+
224
+ $ time ruby examples/msgpack.rb
225
+
226
+ real 2m36.408s
227
+ user 0m8.249s
228
+ sys 0m4.441s
229
+
230
+ ### in_http_ex
231
+
232
+ json
233
+
234
+ $ time ruby examples/json.rb
235
+
236
+ real 2m30.639s
237
+ user 0m7.195s
238
+ sys 0m4.686s
239
+
240
+ msgpack
241
+
242
+ $ time ruby examples/msgpack.rb
243
+
244
+ real 2m28.442s
245
+ user 0m7.126s
246
+ sys 0m4.324s
247
+
248
+ json list
249
+
250
+ $ time ruby examples/json_list.rb
251
+
252
+ real 0m18.179s
253
+ user 0m0.872s
254
+ sys 0m0.477s
255
+
256
+ msgpack list
257
+
258
+ $ time ruby examples/msgpack_list.rb
259
+
260
+ real 0m13.787s
261
+ user 0m0.908s
262
+ sys 0m0.470s
263
+
264
+ msgpack chunked
265
+
266
+ $ time ruby examples/nc_chunked.rb
267
+
268
+ real 0m1.584s
269
+ user 0m0.244s
270
+ sys 0m0.107s
271
+
272
+
273
+ ## Copyright
274
+
275
+ Copyright (c) 2013 hiro-su.
276
+
277
+ Based on the in_http plugin by FURUHASHI Sadayuki
278
+
279
+ Apache License, Version 2.0
data/Rakefile ADDED
@@ -0,0 +1,10 @@
1
+ #!/usr/bin/env rake
2
+ require "bundler/gem_tasks"
3
+ require 'rake/testtask'
4
+ Rake::TestTask.new(:test) do |test|
5
+ test.libs << 'lib' << 'test'
6
+ test.pattern = 'test/**/test_*.rb'
7
+ test.verbose = true
8
+ end
9
+
10
+ task :default => :test
@@ -0,0 +1,27 @@
1
+ require 'net/http'
2
+ require 'stringio'
3
+ require 'msgpack'
4
+ require 'json'
5
+
6
+ host, port = 'localhost', 8888
7
+ path = "/ms/test.tag"
8
+ http = Net::HTTP.new(host, port)
9
+ req = Net::HTTP::Post.new(path)
10
+ req[ "Content-Type" ] = 'application/x-msgpack'
11
+ req[ "Transfer-Encoding" ] = "chunked"
12
+ req[ "Connection" ] = "Keep-Alive"
13
+
14
+ io = StringIO.new
15
+ DATA.each do |line|
16
+ io << JSON.parse(line.chomp).to_msgpack
17
+ end
18
+ io.rewind
19
+ req.body_stream = io
20
+ http.request(req).body
21
+
22
+ __END__
23
+ {"key1":"value1"}
24
+ {"key2":"value2"}
25
+ {"key3":"value3"}
26
+ {"key4":"value4"}
27
+ {"key5":"value5"}
data/examples/json.rb ADDED
@@ -0,0 +1,23 @@
1
+ require 'net/http'
2
+
3
+ # in_http
4
+ #host, port = 'localhost', 7777
5
+ #path = "/test.tag"
6
+ #http = Net::HTTP.new(host, port)
7
+ #req = Net::HTTP::Post.new(path)
8
+ #req["Content-Type"] = "application/json"
9
+ #1.upto(10000) do |i|
10
+ # req.body = "{\"key#{i}\":\"value#{i}\"}"
11
+ # http.request(req)
12
+ #end
13
+
14
+ # in_http_ex
15
+ host, port = 'localhost', 8888
16
+ path = "/j/test.tag"
17
+ http = Net::HTTP.new(host, port)
18
+ req = Net::HTTP::Post.new(path)
19
+ req["Content-Type"] = "application/json"
20
+ 1.upto(10000) do |i|
21
+ req.body = "{\"key#{i}\":\"value#{i}\"}"
22
+ http.request(req)
23
+ end
@@ -0,0 +1,12 @@
1
+ require 'net/http'
2
+ require 'msgpack'
3
+
4
+ host, port = 'localhost', 8888
5
+ path = "/js/test.tag"
6
+ http = Net::HTTP.new(host, port)
7
+ req = Net::HTTP::Post.new(path)
8
+ req["Content-Type"] = "application/json"
9
+ 1.upto(1000) do |i|
10
+ req.body = "[{\"key#{i}\":\"value#{i}\"},{\"key#{i}\":\"value#{i}\"},{\"key#{i}\":\"value#{i}\"},{\"key#{i}\":\"value#{i}\"},{\"key#{i}\":\"value#{i}\"},{\"key#{i}\":\"value#{i}\"},{\"key#{i}\":\"value#{i}\"},{\"key#{i}\":\"value#{i}\"},{\"key#{i}\":\"value#{i}\"},{\"key#{i}\":\"value#{i}\"}]"
11
+ http.request(req)
12
+ end
@@ -0,0 +1,22 @@
1
+ require 'net/http'
2
+ require 'msgpack'
3
+
4
+ # in_http
5
+ #host, port = 'localhost', 7777
6
+ #path = "/test.tag"
7
+ #http = Net::HTTP.new(host, port)
8
+ #1.upto(10000) do |i|
9
+ # record = URI.encode_www_form({"msgpack"=>{"key#{i}"=>"value#{i}"}.to_msgpack})
10
+ # http.post(path, record)
11
+ #end
12
+
13
+ # in_http_ex
14
+ host, port = 'localhost', 8888
15
+ path = "/m/test.tag"
16
+ http = Net::HTTP.new(host, port)
17
+ req = Net::HTTP::Post.new(path)
18
+ req["Content-Type"] = "application/x-msgpack"
19
+ 1.upto(10000) do |i|
20
+ req.body = {"key#{i}"=>"value#{i}"}.to_msgpack
21
+ http.request(req)
22
+ end
@@ -0,0 +1,23 @@
1
+ require 'net/http'
2
+ require 'msgpack'
3
+
4
+ host, port = 'localhost', 8888
5
+ path = "/ms/test.tag"
6
+ http = Net::HTTP.new(host, port)
7
+ req = Net::HTTP::Post.new(path)
8
+ req["Content-Type"] = "application/x-msgpack"
9
+ 1.upto(1000) do |i|
10
+ req.body = [
11
+ {"key#{i}"=>"value#{i}"},
12
+ {"key#{i}"=>"value#{i}"},
13
+ {"key#{i}"=>"value#{i}"},
14
+ {"key#{i}"=>"value#{i}"},
15
+ {"key#{i}"=>"value#{i}"},
16
+ {"key#{i}"=>"value#{i}"},
17
+ {"key#{i}"=>"value#{i}"},
18
+ {"key#{i}"=>"value#{i}"},
19
+ {"key#{i}"=>"value#{i}"},
20
+ {"key#{i}"=>"value#{i}"},
21
+ ].to_msgpack
22
+ http.request(req)
23
+ end
@@ -0,0 +1,44 @@
1
+ require 'open3'
2
+ require 'msgpack'
3
+
4
+ class ChunkTest
5
+ def initialize(host, port)
6
+ @host = host
7
+ @port = port
8
+ @cmd = "nc"
9
+ @term = false
10
+ end
11
+
12
+ def run
13
+ Signal.trap(:INT){
14
+ @term = true
15
+ }
16
+ Open3.popen3("#{@cmd} #{@host} #{@port}") do |stdin, stdout, stderr, wait_thr|
17
+ begin
18
+ i = 0
19
+ loop do
20
+ break if @term
21
+ body = {"key#{i}"=>"value#{i}"}.to_msgpack
22
+ size = body.size
23
+ head = \
24
+ "POST /ms/test.tag HTTP/1.1\r\nUser-Agent: curl/7.28.0\r\nHost: #{@host}:#{@port}\r\nContent-type: application/x-msgpack\r\nTransfer-Encoding: chunked\r\nConnection: Keep-Alive\r\nExpect: 100-continue\r\n\r\n"
25
+ if i == 0
26
+ stdin << head
27
+ elsif i > 10000
28
+ stdin << "0\r\n\r\n"
29
+ break
30
+ else
31
+ chunk = "#{size.to_s(16)}\r\n#{body}\r\n"
32
+ stdin << chunk
33
+ end
34
+ i += 1
35
+ end
36
+ ensure
37
+ stdin.close
38
+ end
39
+ end
40
+ end
41
+ end
42
+
43
+ chunk = ChunkTest.new('localhost', 8888)
44
+ chunk.run
@@ -0,0 +1,11 @@
1
+ <source>
2
+ type http_ex
3
+ port 8888
4
+ bind 0.0.0.0
5
+ body_size_limit 300M
6
+ keepalive_timeout 0s
7
+ </source>
8
+
9
+ <match **>
10
+ type stdout
11
+ </match>
@@ -0,0 +1,21 @@
1
+ # coding: 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 |spec|
6
+ spec.name = "fluent-plugin-http-ex"
7
+ spec.version = "0.0.1"
8
+ spec.authors = ["hiro-su"]
9
+ spec.email = ["h.sugipon@gmail.com"]
10
+ spec.description = %q{fluent plugin to accept multiple json/msgpack events in HTTP request}
11
+ spec.summary = %q{fluent plugin to accept multiple json/msgpack events in HTTP request}
12
+ spec.homepage = "https://github.com/hiro-su/fluent-plugin-http-ex"
13
+
14
+ spec.files = `git ls-files`.split($/)
15
+ spec.executables = spec.files.grep(%r{^bin/}) { |f| File.basename(f) }
16
+ spec.test_files = spec.files.grep(%r{^(test|spec|features)/})
17
+ spec.require_paths = ["lib"]
18
+
19
+ spec.add_development_dependency "fluentd"
20
+ spec.add_runtime_dependency "fluentd"
21
+ end
@@ -0,0 +1,252 @@
1
+ module Fluent
2
+
3
+ class ExHttpInput < HttpInput
4
+ Plugin.register_input('http_ex', self)
5
+
6
+ config_param :port, :integer, :default => 8888
7
+ config_param :bind, :string, :default => '0.0.0.0'
8
+ config_param :body_size_limit, :size, :default => 32*1024*1024
9
+ config_param :keepalive_timeout, :time, :default => 30
10
+
11
+ class KeepaliveManager < Coolio::TimerWatcher
12
+ class TimerValue
13
+ def initialize
14
+ @value = 0
15
+ end
16
+ attr_accessor :value
17
+ end
18
+
19
+ def initialize(timeout)
20
+ super(1, true)
21
+ @cons = {}
22
+ @timeout = timeout.to_i
23
+ end
24
+
25
+ def add(sock)
26
+ @cons[sock] = sock
27
+ end
28
+
29
+ def delete(sock)
30
+ @cons.delete(sock)
31
+ end
32
+
33
+ def on_timer
34
+ @cons.each_pair {|sock,val|
35
+ if sock.step_idle > @timeout
36
+ sock.close unless @timeout == 0
37
+ end
38
+ }
39
+ end
40
+ end
41
+
42
+ def start
43
+ $log.debug "listening http on #{@bind}:#{@port}"
44
+ lsock = TCPServer.new(@bind, @port)
45
+
46
+ detach_multi_process do
47
+ Input.new.start
48
+ @km = KeepaliveManager.new(@keepalive_timeout)
49
+ @lsock = Coolio::TCPServer.new(lsock, nil, ExHandler, @km, method(:on_request), @body_size_limit)
50
+
51
+ @loop = Coolio::Loop.new
52
+ @loop.attach(@km)
53
+ @loop.attach(@lsock)
54
+
55
+ @thread = Thread.new(&method(:run))
56
+ end
57
+ end
58
+
59
+ def on_request(path_info, params)
60
+ $log.debug "remote_addr: #{params["REMOTE_ADDR"]}, path_info: #{path_info}"
61
+ begin
62
+ path = path_info[1..-1] # remove /
63
+ resource, tag = path.split('/')
64
+ tag ||= resource
65
+
66
+ if chunk = params['chunked']
67
+ record = chunk
68
+
69
+ elsif js = params['json']
70
+ record = JSON.parse(js)
71
+
72
+ elsif ms = params['x-msgpack'] || ms = params['msgpack']
73
+ record = MessagePack::unpack(ms)
74
+
75
+ else
76
+ raise "'json' or 'msgpack' parameter is required"
77
+ end
78
+
79
+ time = params['time']
80
+ time = time.to_i
81
+ if time == 0
82
+ time = Engine.now
83
+ end
84
+
85
+ rescue
86
+ return ["400 Bad Request", {'Content-type'=>'text/plain'}, "400 Bad Request\n#{$!}\n"]
87
+ end
88
+
89
+ # TODO server error
90
+ begin
91
+ case params["resource"]
92
+ when :js
93
+ record.each do |v|
94
+ line = begin
95
+ JSON.parse(v)
96
+ rescue TypeError
97
+ v #hash
98
+ end
99
+ Engine.emit(tag, time, line)
100
+ end
101
+
102
+ when :ms
103
+ record.each {|line| Engine.emit(tag, time, line) }
104
+
105
+ else
106
+ Engine.emit(tag, time, record)
107
+ end
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 ExHandler < Handler
117
+ def on_close
118
+ $log.debug "close #{@remote_addr}:#{@remote_port}"
119
+ super
120
+ end
121
+
122
+ def on_headers_complete(headers)
123
+ expect = nil
124
+ size = nil
125
+ if @parser.http_version == [1, 1]
126
+ @keep_alive = true
127
+ else
128
+ @keep_alive = false
129
+ end
130
+ @env = {}
131
+ headers.each_pair {|k,v|
132
+ @env["HTTP_#{k.gsub('-','_').upcase}"] = v
133
+ case k
134
+ when /Expect/i
135
+ expect = v
136
+ when /Content-Length/i
137
+ size = v.to_i
138
+ when /Content-Type/i
139
+ @content_type = v
140
+ when /Connection/i
141
+ if v =~ /close/i
142
+ @keep_alive = false
143
+ elsif v =~ /Keep-alive/i
144
+ @keep_alive = true
145
+ end
146
+ when /Transfer-Encoding/i
147
+ if v =~ /chunked/i
148
+ @chunked = true
149
+ end
150
+ end
151
+ }
152
+ if expect
153
+ if expect == '100-continue'
154
+ if !size || size < @body_size_limit
155
+ send_response_nobody("100 Continue", {})
156
+ else
157
+ send_response_and_close("413 Request Entity Too Large", {}, "Too large")
158
+ end
159
+ else
160
+ send_response_and_close("417 Expectation Failed", {}, "")
161
+ end
162
+ end
163
+ end
164
+
165
+ def on_body(chunk)
166
+ if @chunked && @content_type =~ /application\/x-msgpack/i
167
+ m = method(:on_read_msgpack)
168
+ @u = MessagePack::Unpacker.new
169
+ (class << self; self; end).module_eval do
170
+ define_method(:on_body, m)
171
+ end
172
+ m.call(chunk)
173
+ else
174
+ if @body.bytesize + chunk.bytesize > @body_size_limit
175
+ unless closing?
176
+ send_response_and_close("413 Request Entity Too Large", {}, "Too large")
177
+ end
178
+ return
179
+ end
180
+ @body << chunk
181
+ end
182
+ end
183
+
184
+ def on_read_msgpack(data)
185
+ params = WEBrick::HTTPUtils.parse_query(@parser.query_string)
186
+ path_info = @parser.request_path
187
+ @u.feed_each(data) do |obj|
188
+ params["chunked"] = obj
189
+ params["REMOTE_ADDR"] = @remote_addr
190
+ @callback.call(path_info, params)
191
+ end
192
+ rescue
193
+ $log.error "on_read_msgpack error: #{$!.to_s}"
194
+ $log.error_backtrace
195
+ close
196
+ end
197
+
198
+ def on_message_complete
199
+ return if closing?
200
+
201
+ @env['REMOTE_ADDR'] = @remote_addr
202
+
203
+ params = WEBrick::HTTPUtils.parse_query(@parser.query_string)
204
+ path_info = @parser.request_path
205
+
206
+ params = check_content_type(params, @content_type, @body, path_info)
207
+ params.merge!(@env)
208
+ @env.clear
209
+
210
+ unless @chunked
211
+ code, header, body = *@callback.call(path_info, params)
212
+ body = body.to_s
213
+
214
+ if @keep_alive
215
+ header['Connection'] = 'Keep-Alive'
216
+ send_response(code, header, body)
217
+ else
218
+ send_response_and_close(code, header, body)
219
+ end
220
+ else
221
+ send_response("200 OK", {'Content-type'=>'text/plain', 'Connection'=>'Keep-Alive'}, "")
222
+ end
223
+ end
224
+
225
+ def check_content_type(params, content_type, body, path_info)
226
+ path = path_info[1..-1] # remove /
227
+ resource, _ = path.split('/')
228
+ case resource
229
+ when /^js$/
230
+ params["resource"] = :js
231
+ when /^ms$/
232
+ params["resource"] = :ms
233
+ end
234
+
235
+ if content_type =~ /^application\/x-www-form-urlencoded/
236
+ params.update WEBrick::HTTPUtils.parse_query(body)
237
+ elsif content_type =~ /^multipart\/form-data; boundary=(.+)/
238
+ boundary = WEBrick::HTTPUtils.dequote($1)
239
+ params.update WEBrick::HTTPUtils.parse_form_data(body, boundary)
240
+ elsif content_type =~ /^application\/(json|x-msgpack)/
241
+ params[$1] = body
242
+ end
243
+
244
+ params
245
+ rescue => ex
246
+ $log.error ex
247
+ $log.error ex.backtrace * "\n"
248
+ end
249
+ end
250
+ end
251
+
252
+ end
data/test/helper.rb ADDED
@@ -0,0 +1,23 @@
1
+ require 'test/unit'
2
+
3
+ $LOAD_PATH.unshift(File.join(File.dirname(__FILE__), '..', 'lib'))
4
+ $LOAD_PATH.unshift(File.dirname(__FILE__))
5
+
6
+ require 'fluent/test'
7
+ require 'in_http_ex_test'
8
+
9
+ unless ENV.has_key?('VERBOSE')
10
+ nulllogger = Object.new
11
+ nulllogger.instance_eval {|obj|
12
+ def method_missing(method, *args)
13
+ # pass
14
+ end
15
+ }
16
+ $log = nulllogger
17
+ end
18
+
19
+ require 'fluent/plugin/in_http'
20
+ require 'fluent/plugin/in_http_ex'
21
+
22
+ class Test::Unit::TestCase
23
+ end
@@ -0,0 +1,100 @@
1
+ #
2
+ # Fluent
3
+ #
4
+ # Copyright (C) 2011 FURUHASHI Sadayuki
5
+ #
6
+ # Licensed under the Apache License, Version 2.0 (the "License");
7
+ # you may not use this file except in compliance with the License.
8
+ # You may obtain a copy of the License at
9
+ #
10
+ # http://www.apache.org/licenses/LICENSE-2.0
11
+ #
12
+ # Unless required by applicable law or agreed to in writing, software
13
+ # distributed under the License is distributed on an "AS IS" BASIS,
14
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
15
+ # See the License for the specific language governing permissions and
16
+ # limitations under the License.
17
+ #
18
+ module Fluent
19
+ module Test
20
+
21
+
22
+ class ExHttpInputTestDriver < TestDriver
23
+ def initialize(klass, &block)
24
+ super(klass, &block)
25
+ @emit_streams = []
26
+ @expects = nil
27
+ end
28
+
29
+ def expect_emit(tag, time, record)
30
+ (@expects ||= []) << [tag, time, record]
31
+ self
32
+ end
33
+
34
+ def expected_emits
35
+ @expects ||= []
36
+ end
37
+
38
+ attr_reader :emit_streams
39
+
40
+ def emits
41
+ all = []
42
+ @emit_streams.each {|tag,events|
43
+ events.each {|time,record|
44
+ all << [tag, time, record]
45
+ }
46
+ }
47
+ all
48
+ end
49
+
50
+ def events
51
+ all = []
52
+ @emit_streams.each {|tag,events|
53
+ all.concat events
54
+ }
55
+ all
56
+ end
57
+
58
+ def records
59
+ all = []
60
+ @emit_streams.each {|tag,events|
61
+ events.each {|time,record|
62
+ all << record
63
+ }
64
+ }
65
+ all
66
+ end
67
+
68
+ def run(&block)
69
+ m = method(:emit_stream)
70
+ super {
71
+ Engine.define_singleton_method(:emit_stream) {|tag,es|
72
+ m.call(tag, es)
73
+ }
74
+
75
+ block.call if block
76
+
77
+ if @expects && @emit_streams.size < 2
78
+ i = 0
79
+ @emit_streams.each {|tag,events|
80
+ events.each {|time,record|
81
+ assert_equal(@expects[i], [tag, time, record])
82
+ }
83
+ }
84
+ assert_equal @expects.length, i
85
+ elsif @expects
86
+ return [@expects, @emit_streams]
87
+ end
88
+ }
89
+ self
90
+ end
91
+
92
+ private
93
+ def emit_stream(tag, es)
94
+ @emit_streams << [tag, es.to_a]
95
+ end
96
+ end
97
+
98
+
99
+ end
100
+ end
@@ -0,0 +1,347 @@
1
+ require 'helper'
2
+ require 'net/http'
3
+
4
+ class ExHttpInputTest < Test::Unit::TestCase
5
+ def setup
6
+ Fluent::Test.setup
7
+ end
8
+
9
+ CONFIG = %[
10
+ port 9911
11
+ bind 127.0.0.1
12
+ body_size_limit 10m
13
+ keepalive_timeout 5
14
+ ]
15
+
16
+ def create_driver(conf=CONFIG)
17
+ Fluent::Test::ExHttpInputTestDriver.new(Fluent::ExHttpInput).configure(conf)
18
+ end
19
+
20
+ def test_configure
21
+ d = create_driver
22
+ assert_equal 9911, d.instance.port
23
+ assert_equal '127.0.0.1', d.instance.bind
24
+ assert_equal 10*1024*1024, d.instance.body_size_limit
25
+ assert_equal 5, d.instance.keepalive_timeout
26
+ end
27
+
28
+ def test_time
29
+ d = create_driver
30
+
31
+ time = Time.parse("2011-01-02 13:14:15 UTC").to_i
32
+ Fluent::Engine.now = time
33
+
34
+ d.expect_emit "tag1", time, {"a"=>1}
35
+ d.expect_emit "tag2", time, {"a"=>2}
36
+
37
+ d.run do
38
+ d.expected_emits.each {|tag,time,record|
39
+ res = post("/#{tag}", {"json"=>record.to_json})
40
+ assert_equal "200", res.code
41
+ }
42
+ end
43
+ end
44
+
45
+ def test_json
46
+ d = create_driver
47
+
48
+ time = Time.parse("2011-01-02 13:14:15 UTC").to_i
49
+
50
+ d.expect_emit "tag1", time, {"a"=>1}
51
+ d.expect_emit "tag2", time, {"a"=>2}
52
+
53
+ d.run do
54
+ d.expected_emits.each {|tag,time,record|
55
+ res = post("/#{tag}", {"json"=>record.to_json, "time"=>time.to_s})
56
+ assert_equal "200", res.code
57
+ }
58
+ end
59
+ end
60
+
61
+ def test_application_json
62
+ d = create_driver
63
+
64
+ time = Time.parse("2011-01-02 13:14:15 UTC").to_i
65
+
66
+ d.expect_emit "tag1", time, {"a"=>1}
67
+ d.expect_emit "tag2", time, {"a"=>2}
68
+
69
+ d.run do
70
+ d.expected_emits.each {|tag,time,record|
71
+ http = Net::HTTP.new("127.0.0.1", 9911)
72
+ req = Net::HTTP::Post.new("/#{tag}?time=#{time.to_s}", {"content-type"=>"application/json; charset=utf-8"})
73
+ req.body = record.to_json
74
+ res = http.request(req)
75
+ assert_equal "200", res.code
76
+ }
77
+ end
78
+ end
79
+
80
+ def test_resource_json
81
+ d = create_driver
82
+
83
+ time = Time.parse("2011-01-02 13:14:15 UTC").to_i
84
+
85
+ d.expect_emit "tag1", time, {"a"=>1}
86
+ d.expect_emit "tag2", time, {"a"=>2}
87
+
88
+ d.run do
89
+ d.expected_emits.each {|tag,time,record|
90
+ res = post("/j/#{tag}", {"json"=>record.to_json, "time"=>time.to_s})
91
+ assert_equal "200", res.code
92
+ }
93
+ end
94
+ end
95
+
96
+ def test_application_resource_json
97
+ d = create_driver
98
+
99
+ time = Time.parse("2011-01-02 13:14:15 UTC").to_i
100
+
101
+ d.expect_emit "tag1", time, {"a"=>1}
102
+ d.expect_emit "tag2", time, {"a"=>2}
103
+
104
+ d.run do
105
+ d.expected_emits.each {|tag,time,record|
106
+ http = Net::HTTP.new("127.0.0.1", 9911)
107
+ req = Net::HTTP::Post.new("/j/#{tag}?time=#{time.to_s}", {"content-type"=>"application/json; charset=utf-8"})
108
+ req.body = record.to_json
109
+ res = http.request(req)
110
+ assert_equal "200", res.code
111
+ }
112
+ end
113
+ end
114
+
115
+ def test_resource_json_list
116
+ d = create_driver
117
+
118
+ time = Time.parse("2011-01-02 13:14:15 UTC").to_i
119
+
120
+ d.expect_emit "tag1", time, {"a"=>1}
121
+ d.expect_emit "tag2", time, {"a"=>2}
122
+ d.expect_emit "tag3", time, {"a"=>3}
123
+
124
+ rs = d.run do
125
+ d.expected_emits.each {|tag,time,record|
126
+ res = post("/js/#{tag}", {"json"=>"[#{record.to_json},#{record.to_json},#{record.to_json}]", "time"=>time.to_s})
127
+ assert_equal "200", res.code
128
+ }
129
+ end
130
+
131
+ expects, results = rs
132
+ i, c = 0, 0
133
+ results.each {|tag,es|
134
+ es.each {|time,record|
135
+ assert_equal(expects[i], [tag, time, record])
136
+ c += 1
137
+ i += 1 if c % 3 == 0
138
+ }
139
+ }
140
+ end
141
+
142
+ def test_application_resource_json_list
143
+ d = create_driver
144
+
145
+ time = Time.parse("2011-01-02 13:14:15 UTC").to_i
146
+
147
+ d.expect_emit "tag1", time, {"a"=>1}
148
+ d.expect_emit "tag2", time, {"a"=>2}
149
+ d.expect_emit "tag3", time, {"a"=>3}
150
+
151
+ rs = d.run do
152
+ d.expected_emits.each {|tag,time,record|
153
+ http = Net::HTTP.new("127.0.0.1", 9911)
154
+ req = Net::HTTP::Post.new("/js/#{tag}?time=#{time.to_s}", {"content-type"=>"application/json; charset=utf-8"})
155
+ req.body = "[#{record.to_json},#{record.to_json},#{record.to_json}]"
156
+ res = http.request(req)
157
+ assert_equal "200", res.code
158
+ }
159
+ end
160
+
161
+ expects, results = rs
162
+ i, c = 0, 0
163
+ results.each {|tag,es|
164
+ es.each {|time,record|
165
+ assert_equal(expects[i], [tag, time, record])
166
+ c += 1
167
+ i += 1 if c % 3 == 0
168
+ }
169
+ }
170
+ end
171
+
172
+ def test_msgpack
173
+ d = create_driver
174
+
175
+ time = Time.parse("2011-01-02 13:14:15 UTC").to_i
176
+
177
+ d.expect_emit "tag1", time, {"a"=>1}
178
+ d.expect_emit "tag2", time, {"a"=>2}
179
+
180
+ d.run do
181
+ d.expected_emits.each {|tag,time,record|
182
+ res = post("/#{tag}", {"msgpack"=>record.to_msgpack, "time"=>time.to_s})
183
+ assert_equal "200", res.code
184
+ }
185
+ end
186
+ end
187
+
188
+ def test_application_msgpack
189
+ d = create_driver
190
+
191
+ time = Time.parse("2011-01-02 13:14:15 UTC").to_i
192
+
193
+ d.expect_emit "tag1", time, {"a"=>1}
194
+ d.expect_emit "tag2", time, {"a"=>2}
195
+
196
+ d.run do
197
+ d.expected_emits.each {|tag,time,record|
198
+ http = Net::HTTP.new("127.0.0.1", 9911)
199
+ req = Net::HTTP::Post.new("/#{tag}?time=#{time.to_s}", {"content-type"=>"application/x-msgpack; charset=utf-8"})
200
+ req.body = record.to_msgpack
201
+ res = http.request(req)
202
+ assert_equal "200", res.code
203
+ }
204
+ end
205
+ end
206
+
207
+ def test_resource_msgpack
208
+ d = create_driver
209
+
210
+ time = Time.parse("2011-01-02 13:14:15 UTC").to_i
211
+
212
+ d.expect_emit "tag1", time, {"a"=>1}
213
+ d.expect_emit "tag2", time, {"a"=>2}
214
+
215
+ d.run do
216
+ d.expected_emits.each {|tag,time,record|
217
+ res = post("/m/#{tag}", {"msgpack"=>record.to_msgpack, "time"=>time.to_s})
218
+ assert_equal "200", res.code
219
+ }
220
+ end
221
+ end
222
+
223
+ def test_application_resource_msgpack
224
+ d = create_driver
225
+
226
+ time = Time.parse("2011-01-02 13:14:15 UTC").to_i
227
+
228
+ d.expect_emit "tag1", time, {"a"=>1}
229
+ d.expect_emit "tag2", time, {"a"=>2}
230
+
231
+ d.run do
232
+ d.expected_emits.each {|tag,time,record|
233
+ http = Net::HTTP.new("127.0.0.1", 9911)
234
+ req = Net::HTTP::Post.new("/m/#{tag}?time=#{time.to_s}", {"content-type"=>"application/x-msgpack; charset=utf-8"})
235
+ req.body = record.to_msgpack
236
+ res = http.request(req)
237
+ assert_equal "200", res.code
238
+ }
239
+ end
240
+ end
241
+
242
+ def test_resource_msgpack_list
243
+ d = create_driver
244
+
245
+ time = Time.parse("2011-01-02 13:14:15 UTC").to_i
246
+
247
+ d.expect_emit "tag1", time, {"a"=>1}
248
+ d.expect_emit "tag2", time, {"a"=>2}
249
+ d.expect_emit "tag3", time, {"a"=>3}
250
+
251
+ rs = d.run do
252
+ d.expected_emits.each {|tag,time,record|
253
+ res = post("/ms/#{tag}", {"msgpack"=>[record,record,record].to_msgpack, "time"=>time.to_s})
254
+ assert_equal "200", res.code
255
+ }
256
+ end
257
+
258
+ expects, results = rs
259
+ i, c = 0, 0
260
+ results.each {|tag,es|
261
+ es.each {|time,record|
262
+ assert_equal(expects[i], [tag, time, record])
263
+ c += 1
264
+ i += 1 if c % 3 == 0
265
+ }
266
+ }
267
+ end
268
+
269
+ def test_application_resource_msgpack_list
270
+ d = create_driver
271
+
272
+ time = Time.parse("2011-01-02 13:14:15 UTC").to_i
273
+
274
+ d.expect_emit "tag1", time, {"a"=>1}
275
+ d.expect_emit "tag2", time, {"a"=>2}
276
+ d.expect_emit "tag3", time, {"a"=>3}
277
+
278
+ rs = d.run do
279
+ d.expected_emits.each {|tag,time,record|
280
+ http = Net::HTTP.new("127.0.0.1", 9911)
281
+ req = Net::HTTP::Post.new("/ms/#{tag}?time=#{time.to_s}", {"content-type"=>"application/x-msgpack; charset=utf-8"})
282
+ req.body = [record,record,record].to_msgpack
283
+ res = http.request(req)
284
+ assert_equal "200", res.code
285
+ }
286
+ end
287
+
288
+ expects, results = rs
289
+ i, c = 0, 0
290
+ results.each {|tag,es|
291
+ es.each {|time,record|
292
+ assert_equal(expects[i], [tag, time, record])
293
+ c += 1
294
+ i += 1 if c % 3 == 0
295
+ }
296
+ }
297
+ end
298
+
299
+ def test_msgpack_chunked
300
+ require 'stringio'
301
+
302
+ d = create_driver
303
+
304
+ time = Time.parse("2011-01-02 13:14:15 UTC").to_i
305
+
306
+ d.expect_emit "tag1", time, {"a"=>1}
307
+ d.expect_emit "tag1", time, {"a"=>2}
308
+ d.expect_emit "tag1", time, {"a"=>3}
309
+
310
+ http = Net::HTTP.new("127.0.0.1", 9911)
311
+
312
+ rs = d.run do
313
+ io = StringIO.new
314
+ req = ""
315
+ d.expected_emits.each {|tag,time,record|
316
+ req = Net::HTTP::Post.new("/ms/#{tag}?time=#{time.to_s}")
317
+ req["Content-Type"] = "application/x-msgpack"
318
+ req["Transfer-Encoding"] = "chunked"
319
+ io << record.to_msgpack
320
+ io << record.to_msgpack
321
+ io << record.to_msgpack
322
+ }
323
+ io.rewind
324
+ req.body_stream = io
325
+ res = http.request(req)
326
+ assert_equal "200", res.code
327
+ end
328
+
329
+ expects, results = rs
330
+ i, c = 0, 0
331
+ results.each {|tag,es|
332
+ es.each {|time,record|
333
+ assert_equal(expects[i], [tag, time, record])
334
+ c += 1
335
+ i += 1 if c % 3 == 0
336
+ }
337
+ }
338
+ end
339
+
340
+ def post(path, params)
341
+ http = Net::HTTP.new("127.0.0.1", 9911)
342
+ req = Net::HTTP::Post.new(path, {})
343
+ req.set_form_data(params)
344
+ http.request(req)
345
+ end
346
+
347
+ end
metadata ADDED
@@ -0,0 +1,91 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: fluent-plugin-http-ex
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.0.1
5
+ platform: ruby
6
+ authors:
7
+ - hiro-su
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain: []
11
+ date: 2013-08-07 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: '0'
20
+ type: :development
21
+ prerelease: false
22
+ version_requirements: !ruby/object:Gem::Requirement
23
+ requirements:
24
+ - - ! '>='
25
+ - !ruby/object:Gem::Version
26
+ version: '0'
27
+ - !ruby/object:Gem::Dependency
28
+ name: fluentd
29
+ requirement: !ruby/object:Gem::Requirement
30
+ requirements:
31
+ - - ! '>='
32
+ - !ruby/object:Gem::Version
33
+ version: '0'
34
+ type: :runtime
35
+ prerelease: false
36
+ version_requirements: !ruby/object:Gem::Requirement
37
+ requirements:
38
+ - - ! '>='
39
+ - !ruby/object:Gem::Version
40
+ version: '0'
41
+ description: fluent plugin to accept multiple json/msgpack events in HTTP request
42
+ email:
43
+ - h.sugipon@gmail.com
44
+ executables: []
45
+ extensions: []
46
+ extra_rdoc_files: []
47
+ files:
48
+ - .gitignore
49
+ - Gemfile
50
+ - LICENSE.txt
51
+ - README.md
52
+ - Rakefile
53
+ - examples/chunked.rb
54
+ - examples/json.rb
55
+ - examples/json_list.rb
56
+ - examples/msgpack.rb
57
+ - examples/msgpack_list.rb
58
+ - examples/nc_chunked.rb
59
+ - examples/sample.conf
60
+ - fluent-plugin-http-ex.gemspec
61
+ - lib/fluent/plugin/in_http_ex.rb
62
+ - test/helper.rb
63
+ - test/in_http_ex_test.rb
64
+ - test/plugin/test_in_http_ex.rb
65
+ homepage: https://github.com/hiro-su/fluent-plugin-http-ex
66
+ licenses: []
67
+ metadata: {}
68
+ post_install_message:
69
+ rdoc_options: []
70
+ require_paths:
71
+ - lib
72
+ required_ruby_version: !ruby/object:Gem::Requirement
73
+ requirements:
74
+ - - ! '>='
75
+ - !ruby/object:Gem::Version
76
+ version: '0'
77
+ required_rubygems_version: !ruby/object:Gem::Requirement
78
+ requirements:
79
+ - - ! '>='
80
+ - !ruby/object:Gem::Version
81
+ version: '0'
82
+ requirements: []
83
+ rubyforge_project:
84
+ rubygems_version: 2.0.3
85
+ signing_key:
86
+ specification_version: 4
87
+ summary: fluent plugin to accept multiple json/msgpack events in HTTP request
88
+ test_files:
89
+ - test/helper.rb
90
+ - test/in_http_ex_test.rb
91
+ - test/plugin/test_in_http_ex.rb