fluent-plugin-out-http 1.3.0 → 1.3.1

Sign up to get free protection for your applications and to get access to all the features.
@@ -1,297 +1,314 @@
1
- require 'net/http'
2
- require 'uri'
3
- require 'yajl'
4
- require 'fluent/plugin/output'
5
- require 'openssl'
6
-
7
- class Fluent::Plugin::HTTPOutput < Fluent::Plugin::Output
8
- Fluent::Plugin.register_output('http', self)
9
-
10
- class RecoverableResponse < StandardError; end
11
-
12
- helpers :compat_parameters, :formatter
13
-
14
- DEFAULT_BUFFER_TYPE = "memory"
15
- DEFAULT_FORMATTER = "json"
16
-
17
- def initialize
18
- super
19
- end
20
-
21
- # Endpoint URL ex. http://localhost.local/api/
22
- config_param :endpoint_url, :string
23
-
24
- # Set Net::HTTP.verify_mode to `OpenSSL::SSL::VERIFY_NONE`
25
- config_param :ssl_no_verify, :bool, :default => false
26
-
27
- # HTTP method
28
- config_param :http_method, :enum, list: [:get, :put, :post, :delete], :default => :post
29
-
30
- # form | json | text | raw
31
- config_param :serializer, :enum, list: [:json, :form, :text, :raw], :default => :form
32
-
33
- # Simple rate limiting: ignore any records within `rate_limit_msec`
34
- # since the last one.
35
- config_param :rate_limit_msec, :integer, :default => 0
36
-
37
- # Raise errors that were rescued during HTTP requests?
38
- config_param :raise_on_error, :bool, :default => true
39
-
40
- # Specify recoverable error codes
41
- config_param :recoverable_status_codes, :array, value_type: :integer, default: [503]
42
-
43
- # ca file to use for https request
44
- config_param :cacert_file, :string, :default => ''
45
-
46
- # specify client sertificate
47
- config_param :client_cert_path, :string, :default => ''
48
-
49
- # specify private key path
50
- config_param :private_key_path, :string, :default => ''
51
-
52
- # specify private key passphrase
53
- config_param :private_key_passphrase, :string, :default => '', :secret => true
54
-
55
- # custom headers
56
- config_param :custom_headers, :hash, :default => nil
57
-
58
- # 'none' | 'basic' | 'jwt' | 'bearer'
59
- config_param :authentication, :enum, list: [:none, :basic, :jwt, :bearer], :default => :none
60
- config_param :username, :string, :default => ''
61
- config_param :password, :string, :default => '', :secret => true
62
- config_param :token, :string, :default => ''
63
- # Switch non-buffered/buffered plugin
64
- config_param :buffered, :bool, :default => false
65
- config_param :bulk_request, :bool, :default => false
66
-
67
- config_section :buffer do
68
- config_set_default :@type, DEFAULT_BUFFER_TYPE
69
- config_set_default :chunk_keys, ['tag']
70
- end
71
-
72
- config_section :format do
73
- config_set_default :@type, DEFAULT_FORMATTER
74
- end
75
-
76
- def configure(conf)
77
- compat_parameters_convert(conf, :buffer, :formatter)
78
- super
79
-
80
- @ssl_verify_mode = if @ssl_no_verify
81
- OpenSSL::SSL::VERIFY_NONE
82
- else
83
- OpenSSL::SSL::VERIFY_PEER
84
- end
85
-
86
- @ca_file = @cacert_file
87
- @last_request_time = nil
88
- raise Fluent::ConfigError, "'tag' in chunk_keys is required." if !@chunk_key_tag && @buffered
89
-
90
- if @formatter_config = conf.elements('format').first
91
- @formatter = formatter_create
92
- end
93
-
94
- if @bulk_request
95
- class << self
96
- alias_method :format, :bulk_request_format
97
- end
98
- @formatter = formatter_create(type: :json)
99
- @serializer = :x_ndjson # secret settings for bulk_request
100
- else
101
- class << self
102
- alias_method :format, :split_request_format
103
- end
104
- end
105
- end
106
-
107
- def start
108
- super
109
- end
110
-
111
- def shutdown
112
- super
113
- end
114
-
115
- def format_url(tag, time, record)
116
- @endpoint_url
117
- end
118
-
119
- def set_body(req, tag, time, record)
120
- if @serializer == :json
121
- set_json_body(req, record)
122
- elsif @serializer == :text
123
- set_text_body(req, record)
124
- elsif @serializer == :raw
125
- set_raw_body(req, record)
126
- elsif @serializer == :x_ndjson
127
- set_bulk_body(req, record)
128
- else
129
- req.set_form_data(record)
130
- end
131
- req
132
- end
133
-
134
- def set_header(req, tag, time, record)
135
- if @custom_headers
136
- @custom_headers.each do |k,v|
137
- req[k] = v
138
- end
139
- req
140
- else
141
- req
142
- end
143
- end
144
-
145
- def set_json_body(req, data)
146
- req.body = Yajl.dump(data)
147
- req['Content-Type'] = 'application/json'
148
- end
149
-
150
- def set_text_body(req, data)
151
- req.body = data["message"]
152
- req['Content-Type'] = 'text/plain'
153
- end
154
-
155
- def set_raw_body(req, data)
156
- req.body = data.to_s
157
- req['Content-Type'] = 'application/octet-stream'
158
- end
159
-
160
- def set_bulk_body(req, data)
161
- req.body = data.to_s
162
- req['Content-Type'] = 'application/x-ndjson'
163
- end
164
-
165
- def create_request(tag, time, record)
166
- url = format_url(tag, time, record)
167
- uri = URI.parse(url)
168
- req = Net::HTTP.const_get(@http_method.to_s.capitalize).new(uri.request_uri)
169
- set_body(req, tag, time, record)
170
- set_header(req, tag, time, record)
171
- return req, uri
172
- end
173
-
174
- def http_opts(uri)
175
- opts = {
176
- :use_ssl => uri.scheme == 'https'
177
- }
178
- opts[:verify_mode] = @ssl_verify_mode if opts[:use_ssl]
179
- opts[:ca_file] = File.join(@ca_file) if File.file?(@ca_file)
180
- opts[:cert] = OpenSSL::X509::Certificate.new(File.read(@client_cert_path)) if File.file?(@client_cert_path)
181
- opts[:key] = OpenSSL::PKey.read(File.read(@private_key_path), @private_key_passphrase) if File.file?(@private_key_path)
182
- opts
183
- end
184
-
185
- def proxies
186
- ENV['HTTPS_PROXY'] || ENV['HTTP_PROXY'] || ENV['http_proxy'] || ENV['https_proxy']
187
- end
188
-
189
- def send_request(req, uri)
190
- is_rate_limited = (@rate_limit_msec != 0 and not @last_request_time.nil?)
191
- if is_rate_limited and ((Time.now.to_f - @last_request_time) * 1000.0 < @rate_limit_msec)
192
- log.info('Dropped request due to rate limiting')
193
- return
194
- end
195
-
196
- res = nil
197
-
198
- begin
199
- if @authentication == :basic
200
- req.basic_auth(@username, @password)
201
- elsif @authentication == :bearer
202
- req['authorization'] = "bearer #{@token}"
203
- elsif @authentication == :jwt
204
- req['authorization'] = "jwt #{@token}"
205
- end
206
- @last_request_time = Time.now.to_f
207
-
208
- if proxy = proxies
209
- proxy_uri = URI.parse(proxy)
210
-
211
- res = Net::HTTP.start(uri.host, uri.port,
212
- proxy_uri.host, proxy_uri.port, proxy_uri.user, proxy_uri.password,
213
- **http_opts(uri)) {|http| http.request(req) }
214
- else
215
- res = Net::HTTP.start(uri.host, uri.port, **http_opts(uri)) {|http| http.request(req) }
216
- end
217
-
218
- rescue => e # rescue all StandardErrors
219
- # server didn't respond
220
- log.warn "Net::HTTP.#{req.method.capitalize} raises exception: #{e.class}, '#{e.message}'"
221
- raise e if @raise_on_error
222
- else
223
- unless res and res.is_a?(Net::HTTPSuccess)
224
- res_summary = if res
225
- "#{res.code} #{res.message} #{res.body}"
226
- else
227
- "res=nil"
228
- end
229
- if @recoverable_status_codes.include?(res.code.to_i)
230
- raise RecoverableResponse, res_summary
231
- else
232
- log.warn "failed to #{req.method} #{uri} (#{res_summary})"
233
- end
234
- end #end unless
235
- end # end begin
236
- end # end send_request
237
-
238
- def handle_record(tag, time, record)
239
- if @formatter_config
240
- record = @formatter.format(tag, time, record)
241
- end
242
- req, uri = create_request(tag, time, record)
243
- send_request(req, uri)
244
- end
245
-
246
- def handle_records(tag, time, chunk)
247
- req, uri = create_request(tag, time, chunk.read)
248
- send_request(req, uri)
249
- end
250
-
251
- def prefer_buffered_processing
252
- @buffered
253
- end
254
-
255
- def format(tag, time, record)
256
- # For safety.
257
- end
258
-
259
- def split_request_format(tag, time, record)
260
- [time, record].to_msgpack
261
- end
262
-
263
- def bulk_request_format(tag, time, record)
264
- @formatter.format(tag, time, record)
265
- end
266
-
267
- def formatted_to_msgpack_binary?
268
- if @bulk_request
269
- false
270
- else
271
- true
272
- end
273
- end
274
-
275
- def multi_workers_ready?
276
- true
277
- end
278
-
279
- def process(tag, es)
280
- es.each do |time, record|
281
- handle_record(tag, time, record)
282
- end
283
- end
284
-
285
- def write(chunk)
286
- tag = chunk.metadata.tag
287
- @endpoint_url = extract_placeholders(@endpoint_url, chunk)
288
- if @bulk_request
289
- time = Fluent::Engine.now
290
- handle_records(tag, time, chunk)
291
- else
292
- chunk.msgpack_each do |time, record|
293
- handle_record(tag, time, record)
294
- end
295
- end
296
- end
297
- end
1
+ require 'net/http'
2
+ require 'uri'
3
+ require 'yajl'
4
+ require 'fluent/plugin/output'
5
+ require 'tempfile'
6
+ require 'openssl'
7
+ require 'zlib'
8
+
9
+ class Fluent::Plugin::HTTPOutput < Fluent::Plugin::Output
10
+ Fluent::Plugin.register_output('http', self)
11
+
12
+ class RecoverableResponse < StandardError; end
13
+
14
+ helpers :compat_parameters, :formatter
15
+
16
+ DEFAULT_BUFFER_TYPE = "memory"
17
+ DEFAULT_FORMATTER = "json"
18
+
19
+ def initialize
20
+ super
21
+ end
22
+
23
+ # Endpoint URL ex. http://localhost.local/api/
24
+ config_param :endpoint_url, :string
25
+
26
+ # Set Net::HTTP.verify_mode to `OpenSSL::SSL::VERIFY_NONE`
27
+ config_param :ssl_no_verify, :bool, :default => false
28
+
29
+ # HTTP method
30
+ config_param :http_method, :enum, list: [:get, :put, :post, :delete], :default => :post
31
+
32
+ # form | json | text | raw
33
+ config_param :serializer, :enum, list: [:json, :form, :text, :raw], :default => :form
34
+
35
+ # Simple rate limiting: ignore any records within `rate_limit_msec`
36
+ # since the last one.
37
+ config_param :rate_limit_msec, :integer, :default => 0
38
+
39
+ # Raise errors that were rescued during HTTP requests?
40
+ config_param :raise_on_error, :bool, :default => true
41
+
42
+ # Specify recoverable error codes
43
+ config_param :recoverable_status_codes, :array, value_type: :integer, default: [503]
44
+
45
+ # ca file to use for https request
46
+ config_param :cacert_file, :string, :default => ''
47
+
48
+ # specify client sertificate
49
+ config_param :client_cert_path, :string, :default => ''
50
+
51
+ # specify private key path
52
+ config_param :private_key_path, :string, :default => ''
53
+
54
+ # specify private key passphrase
55
+ config_param :private_key_passphrase, :string, :default => '', :secret => true
56
+
57
+ # custom headers
58
+ config_param :custom_headers, :hash, :default => nil
59
+
60
+ # 'none' | 'basic' | 'jwt' | 'bearer'
61
+ config_param :authentication, :enum, list: [:none, :basic, :jwt, :bearer], :default => :none
62
+ config_param :username, :string, :default => ''
63
+ config_param :password, :string, :default => '', :secret => true
64
+ config_param :token, :string, :default => ''
65
+ # Switch non-buffered/buffered plugin
66
+ config_param :buffered, :bool, :default => false
67
+ config_param :bulk_request, :bool, :default => false
68
+ # Compress with gzip except for form serializer
69
+ config_param :compress_request, :bool, :default => false
70
+
71
+ config_section :buffer do
72
+ config_set_default :@type, DEFAULT_BUFFER_TYPE
73
+ config_set_default :chunk_keys, ['tag']
74
+ end
75
+
76
+ config_section :format do
77
+ config_set_default :@type, DEFAULT_FORMATTER
78
+ end
79
+
80
+ def configure(conf)
81
+ compat_parameters_convert(conf, :buffer, :formatter)
82
+ super
83
+
84
+ @ssl_verify_mode = if @ssl_no_verify
85
+ OpenSSL::SSL::VERIFY_NONE
86
+ else
87
+ OpenSSL::SSL::VERIFY_PEER
88
+ end
89
+
90
+ @ca_file = @cacert_file
91
+ @last_request_time = nil
92
+ raise Fluent::ConfigError, "'tag' in chunk_keys is required." if !@chunk_key_tag && @buffered
93
+
94
+ if @formatter_config = conf.elements('format').first
95
+ @formatter = formatter_create
96
+ end
97
+
98
+ if @bulk_request
99
+ class << self
100
+ alias_method :format, :bulk_request_format
101
+ end
102
+ @formatter = formatter_create(type: :json)
103
+ @serializer = :x_ndjson # secret settings for bulk_request
104
+ else
105
+ class << self
106
+ alias_method :format, :split_request_format
107
+ end
108
+ end
109
+ end
110
+
111
+ def start
112
+ super
113
+ end
114
+
115
+ def shutdown
116
+ super
117
+ end
118
+
119
+ def format_url(tag, time, record)
120
+ @endpoint_url
121
+ end
122
+
123
+ def set_body(req, tag, time, record)
124
+ if @serializer == :json
125
+ set_json_body(req, record)
126
+ elsif @serializer == :text
127
+ set_text_body(req, record)
128
+ elsif @serializer == :raw
129
+ set_raw_body(req, record)
130
+ elsif @serializer == :x_ndjson
131
+ set_bulk_body(req, record)
132
+ else
133
+ req.set_form_data(record)
134
+ end
135
+ req
136
+ end
137
+
138
+ def set_header(req, tag, time, record)
139
+ if @custom_headers
140
+ @custom_headers.each do |k,v|
141
+ req[k] = v
142
+ end
143
+ req
144
+ else
145
+ req
146
+ end
147
+ end
148
+
149
+ def compress_body(req, data)
150
+ return unless @compress_request
151
+ gz = Zlib::GzipWriter.new(StringIO.new)
152
+ gz << data
153
+
154
+ req['Content-Encoding'] = "gzip"
155
+ req.body = gz.close.string
156
+ end
157
+
158
+ def set_json_body(req, data)
159
+ req.body = Yajl.dump(data)
160
+ req['Content-Type'] = 'application/json'
161
+ compress_body(req, req.body)
162
+ end
163
+
164
+ def set_text_body(req, data)
165
+ req.body = data["message"]
166
+ req['Content-Type'] = 'text/plain'
167
+ compress_body(req, req.body)
168
+ end
169
+
170
+ def set_raw_body(req, data)
171
+ req.body = data.to_s
172
+ req['Content-Type'] = 'application/octet-stream'
173
+ compress_body(req, req.body)
174
+ end
175
+
176
+ def set_bulk_body(req, data)
177
+ req.body = data.to_s
178
+ req['Content-Type'] = 'application/x-ndjson'
179
+ compress_body(req, req.body)
180
+ end
181
+
182
+ def create_request(tag, time, record)
183
+ url = format_url(tag, time, record)
184
+ uri = URI.parse(url)
185
+ req = Net::HTTP.const_get(@http_method.to_s.capitalize).new(uri.request_uri)
186
+ set_body(req, tag, time, record)
187
+ set_header(req, tag, time, record)
188
+ return req, uri
189
+ end
190
+
191
+ def http_opts(uri)
192
+ opts = {
193
+ :use_ssl => uri.scheme == 'https'
194
+ }
195
+ opts[:verify_mode] = @ssl_verify_mode if opts[:use_ssl]
196
+ opts[:ca_file] = File.join(@ca_file) if File.file?(@ca_file)
197
+ opts[:cert] = OpenSSL::X509::Certificate.new(File.read(@client_cert_path)) if File.file?(@client_cert_path)
198
+ opts[:key] = OpenSSL::PKey.read(File.read(@private_key_path), @private_key_passphrase) if File.file?(@private_key_path)
199
+ opts
200
+ end
201
+
202
+ def proxies
203
+ ENV['HTTPS_PROXY'] || ENV['HTTP_PROXY'] || ENV['http_proxy'] || ENV['https_proxy']
204
+ end
205
+
206
+ def send_request(req, uri)
207
+ is_rate_limited = (@rate_limit_msec != 0 and not @last_request_time.nil?)
208
+ if is_rate_limited and ((Time.now.to_f - @last_request_time) * 1000.0 < @rate_limit_msec)
209
+ log.info('Dropped request due to rate limiting')
210
+ return
211
+ end
212
+
213
+ res = nil
214
+
215
+ begin
216
+ if @authentication == :basic
217
+ req.basic_auth(@username, @password)
218
+ elsif @authentication == :bearer
219
+ req['authorization'] = "bearer #{@token}"
220
+ elsif @authentication == :jwt
221
+ req['authorization'] = "jwt #{@token}"
222
+ end
223
+ @last_request_time = Time.now.to_f
224
+
225
+ if proxy = proxies
226
+ proxy_uri = URI.parse(proxy)
227
+
228
+ res = Net::HTTP.start(uri.host, uri.port,
229
+ proxy_uri.host, proxy_uri.port, proxy_uri.user, proxy_uri.password,
230
+ **http_opts(uri)) {|http| http.request(req) }
231
+ else
232
+ res = Net::HTTP.start(uri.host, uri.port, **http_opts(uri)) {|http| http.request(req) }
233
+ end
234
+
235
+ rescue => e # rescue all StandardErrors
236
+ # server didn't respond
237
+ log.warn "Net::HTTP.#{req.method.capitalize} raises exception: #{e.class}, '#{e.message}'"
238
+ raise e if @raise_on_error
239
+ else
240
+ unless res and res.is_a?(Net::HTTPSuccess)
241
+ res_summary = if res
242
+ "#{res.code} #{res.message} #{res.body}"
243
+ else
244
+ "res=nil"
245
+ end
246
+ if @recoverable_status_codes.include?(res.code.to_i)
247
+ raise RecoverableResponse, res_summary
248
+ else
249
+ log.warn "failed to #{req.method} #{uri} (#{res_summary})"
250
+ end
251
+ end #end unless
252
+ end # end begin
253
+ end # end send_request
254
+
255
+ def handle_record(tag, time, record)
256
+ if @formatter_config
257
+ record = @formatter.format(tag, time, record)
258
+ end
259
+ req, uri = create_request(tag, time, record)
260
+ send_request(req, uri)
261
+ end
262
+
263
+ def handle_records(tag, time, chunk)
264
+ req, uri = create_request(tag, time, chunk.read)
265
+ send_request(req, uri)
266
+ end
267
+
268
+ def prefer_buffered_processing
269
+ @buffered
270
+ end
271
+
272
+ def format(tag, time, record)
273
+ # For safety.
274
+ end
275
+
276
+ def split_request_format(tag, time, record)
277
+ [time, record].to_msgpack
278
+ end
279
+
280
+ def bulk_request_format(tag, time, record)
281
+ @formatter.format(tag, time, record)
282
+ end
283
+
284
+ def formatted_to_msgpack_binary?
285
+ if @bulk_request
286
+ false
287
+ else
288
+ true
289
+ end
290
+ end
291
+
292
+ def multi_workers_ready?
293
+ true
294
+ end
295
+
296
+ def process(tag, es)
297
+ es.each do |time, record|
298
+ handle_record(tag, time, record)
299
+ end
300
+ end
301
+
302
+ def write(chunk)
303
+ tag = chunk.metadata.tag
304
+ @endpoint_url = extract_placeholders(@endpoint_url, chunk)
305
+ if @bulk_request
306
+ time = Fluent::Engine.now
307
+ handle_records(tag, time, chunk)
308
+ else
309
+ chunk.msgpack_each do |time, record|
310
+ handle_record(tag, time, record)
311
+ end
312
+ end
313
+ end
314
+ end