fluent-plugin-out-http 1.3.0 → 1.3.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.
@@ -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