fluent-plugin-out-http 1.2.0 → 1.3.0

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