fluent-plugin-http-ex 0.0.1

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