fluent-plugin-out-http 1.2.0 → 1.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,246 +1,297 @@
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
- helpers :compat_parameters, :formatter
11
-
12
- DEFAULT_BUFFER_TYPE = "memory"
13
- DEFAULT_FORMATTER = "json"
14
-
15
- def initialize
16
- super
17
- end
18
-
19
- # Endpoint URL ex. http://localhost.local/api/
20
- config_param :endpoint_url, :string
21
-
22
- # Set Net::HTTP.verify_mode to `OpenSSL::SSL::VERIFY_NONE`
23
- config_param :ssl_no_verify, :bool, :default => false
24
-
25
- # HTTP method
26
- config_param :http_method, :enum, list: [:get, :put, :post, :delete], :default => :post
27
-
28
- # form | json | text | raw
29
- config_param :serializer, :enum, list: [:json, :form, :text, :raw], :default => :form
30
-
31
- # Simple rate limiting: ignore any records within `rate_limit_msec`
32
- # since the last one.
33
- config_param :rate_limit_msec, :integer, :default => 0
34
-
35
- # Raise errors that were rescued during HTTP requests?
36
- config_param :raise_on_error, :bool, :default => true
37
-
38
- # ca file to use for https request
39
- config_param :cacert_file, :string, :default => ''
40
-
41
- # specify client sertificate
42
- config_param :client_cert_path, :string, :default => ''
43
-
44
- # specify private key path
45
- config_param :private_key_path, :string, :default => ''
46
-
47
- # specify private key passphrase
48
- config_param :private_key_passphrase, :string, :default => '', :secret => true
49
-
50
- # custom headers
51
- config_param :custom_headers, :hash, :default => nil
52
-
53
- # 'none' | 'basic' | 'jwt' | 'bearer'
54
- config_param :authentication, :enum, list: [:none, :basic, :jwt, :bearer], :default => :none
55
- config_param :username, :string, :default => ''
56
- config_param :password, :string, :default => '', :secret => true
57
- config_param :token, :string, :default => ''
58
- # Switch non-buffered/buffered plugin
59
- config_param :buffered, :bool, :default => false
60
-
61
- config_section :buffer do
62
- config_set_default :@type, DEFAULT_BUFFER_TYPE
63
- config_set_default :chunk_keys, ['tag']
64
- end
65
-
66
- config_section :format do
67
- config_set_default :@type, DEFAULT_FORMATTER
68
- end
69
-
70
- def configure(conf)
71
- compat_parameters_convert(conf, :buffer, :formatter)
72
- super
73
-
74
- @ssl_verify_mode = if @ssl_no_verify
75
- OpenSSL::SSL::VERIFY_NONE
76
- else
77
- OpenSSL::SSL::VERIFY_PEER
78
- end
79
-
80
- @ca_file = @cacert_file
81
- @last_request_time = nil
82
- raise Fluent::ConfigError, "'tag' in chunk_keys is required." if !@chunk_key_tag && @buffered
83
-
84
- if @formatter_config = conf.elements('format').first
85
- @formatter = formatter_create
86
- end
87
- end
88
-
89
- def start
90
- super
91
- end
92
-
93
- def shutdown
94
- super
95
- end
96
-
97
- def format_url(tag, time, record)
98
- @endpoint_url
99
- end
100
-
101
- def set_body(req, tag, time, record)
102
- if @serializer == :json
103
- set_json_body(req, record)
104
- elsif @serializer == :text
105
- set_text_body(req, record)
106
- elsif @serializer == :raw
107
- set_raw_body(req, record)
108
- else
109
- req.set_form_data(record)
110
- end
111
- req
112
- end
113
-
114
- def set_header(req, tag, time, record)
115
- if @custom_headers
116
- @custom_headers.each do |k,v|
117
- req[k] = v
118
- end
119
- req
120
- else
121
- req
122
- end
123
- end
124
-
125
- def set_json_body(req, data)
126
- req.body = Yajl.dump(data)
127
- req['Content-Type'] = 'application/json'
128
- end
129
-
130
- def set_text_body(req, data)
131
- req.body = data["message"]
132
- req['Content-Type'] = 'text/plain'
133
- end
134
-
135
- def set_raw_body(req, data)
136
- req.body = data.to_s
137
- req['Content-Type'] = 'application/octet-stream'
138
- end
139
-
140
- def create_request(tag, time, record)
141
- url = format_url(tag, time, record)
142
- uri = URI.parse(url)
143
- req = Net::HTTP.const_get(@http_method.to_s.capitalize).new(uri.request_uri)
144
- set_body(req, tag, time, record)
145
- set_header(req, tag, time, record)
146
- return req, uri
147
- end
148
-
149
- def http_opts(uri)
150
- opts = {
151
- :use_ssl => uri.scheme == 'https'
152
- }
153
- opts[:verify_mode] = @ssl_verify_mode if opts[:use_ssl]
154
- opts[:ca_file] = File.join(@ca_file) if File.file?(@ca_file)
155
- opts[:cert] = OpenSSL::X509::Certificate.new(File.read(@client_cert_path)) if File.file?(@client_cert_path)
156
- opts[:key] = OpenSSL::PKey::RSA.new(File.read(@private_key_path), @private_key_passphrase) if File.file?(@private_key_path)
157
- opts
158
- end
159
-
160
- def proxies
161
- ENV['HTTPS_PROXY'] || ENV['HTTP_PROXY'] || ENV['http_proxy'] || ENV['https_proxy']
162
- end
163
-
164
- def send_request(req, uri)
165
- is_rate_limited = (@rate_limit_msec != 0 and not @last_request_time.nil?)
166
- if is_rate_limited and ((Time.now.to_f - @last_request_time) * 1000.0 < @rate_limit_msec)
167
- log.info('Dropped request due to rate limiting')
168
- return
169
- end
170
-
171
- res = nil
172
-
173
- begin
174
- if @authentication == :basic
175
- req.basic_auth(@username, @password)
176
- elsif @authentication == :bearer
177
- req['authorization'] = "bearer #{@token}"
178
- elsif @authentication == :jwt
179
- req['authorization'] = "jwt #{@token}"
180
- end
181
- @last_request_time = Time.now.to_f
182
-
183
- if proxy = proxies
184
- proxy_uri = URI.parse(proxy)
185
-
186
- res = Net::HTTP.start(uri.host, uri.port,
187
- proxy_uri.host, proxy_uri.port, proxy_uri.user, proxy_uri.password,
188
- **http_opts(uri)) {|http| http.request(req) }
189
- else
190
- res = Net::HTTP.start(uri.host, uri.port, **http_opts(uri)) {|http| http.request(req) }
191
- end
192
-
193
- rescue => e # rescue all StandardErrors
194
- # server didn't respond
195
- log.warn "Net::HTTP.#{req.method.capitalize} raises exception: #{e.class}, '#{e.message}'"
196
- raise e if @raise_on_error
197
- else
198
- unless res and res.is_a?(Net::HTTPSuccess)
199
- res_summary = if res
200
- "#{res.code} #{res.message} #{res.body}"
201
- else
202
- "res=nil"
203
- end
204
- log.warn "failed to #{req.method} #{uri} (#{res_summary})"
205
- end #end unless
206
- end # end begin
207
- end # end send_request
208
-
209
- def handle_record(tag, time, record)
210
- if @formatter_config
211
- record = @formatter.format(tag, time, record)
212
- end
213
- req, uri = create_request(tag, time, record)
214
- send_request(req, uri)
215
- end
216
-
217
- def prefer_buffered_processing
218
- @buffered
219
- end
220
-
221
- def format(tag, time, record)
222
- [time, record].to_msgpack
223
- end
224
-
225
- def formatted_to_msgpack_binary?
226
- true
227
- end
228
-
229
- def multi_workers_ready?
230
- true
231
- end
232
-
233
- def process(tag, es)
234
- es.each do |time, record|
235
- handle_record(tag, time, record)
236
- end
237
- end
238
-
239
- def write(chunk)
240
- tag = chunk.metadata.tag
241
- @endpoint_url = extract_placeholders(@endpoint_url, chunk)
242
- chunk.msgpack_each do |time, record|
243
- handle_record(tag, time, record)
244
- end
245
- end
246
- end
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