fluent-plugin-scalyr 0.7.4
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/AUTHORS +1 -0
- data/Gemfile +3 -0
- data/LICENSE +202 -0
- data/README.md +166 -0
- data/Rakefile +11 -0
- data/VERSION +1 -0
- data/fluent-plugin-scalyr.gemspec +23 -0
- data/fluent.conf.sample +28 -0
- data/lib/fluent/plugin/out_scalyr.rb +350 -0
- data/lib/fluent/plugin/scalyr-exceptions.rb +24 -0
- data/test/helper.rb +36 -0
- data/test/test_config.rb +58 -0
- data/test/test_events.rb +248 -0
- data/test/test_handle_response.rb +90 -0
- data/test/test_ssl_verify.rb +35 -0
- metadata +139 -0
@@ -0,0 +1,350 @@
|
|
1
|
+
#
|
2
|
+
# Scalyr Output Plugin for Fluentd
|
3
|
+
#
|
4
|
+
# Copyright (C) 2015 Scalyr, Inc.
|
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
|
+
|
19
|
+
require 'fluent/plugin/scalyr-exceptions'
|
20
|
+
require 'json'
|
21
|
+
require 'net/http'
|
22
|
+
require 'net/https'
|
23
|
+
require 'securerandom'
|
24
|
+
require 'thread'
|
25
|
+
|
26
|
+
module Scalyr
|
27
|
+
class ScalyrOut < Fluent::BufferedOutput
|
28
|
+
Fluent::Plugin.register_output( 'scalyr', self )
|
29
|
+
|
30
|
+
config_param :api_write_token, :string
|
31
|
+
config_param :server_attributes, :hash, :default => nil
|
32
|
+
config_param :scalyr_server, :string, :default => "https://agent.scalyr.com/"
|
33
|
+
config_param :ssl_ca_bundle_path, :string, :default => "/etc/ssl/certs/ca-bundle.crt"
|
34
|
+
config_param :ssl_verify_peer, :bool, :default => true
|
35
|
+
config_param :ssl_verify_depth, :integer, :default => 5
|
36
|
+
config_param :message_field, :string, :default => "message"
|
37
|
+
config_param :max_request_buffer, :integer, :default => 1024*1024
|
38
|
+
config_param :force_message_encoding, :string, :default => nil
|
39
|
+
config_param :replace_invalid_utf8, :bool, :default => false
|
40
|
+
|
41
|
+
config_set_default :retry_limit, 40 #try a maximum of 40 times before discarding
|
42
|
+
config_set_default :retry_wait, 5 #wait a minimum of 5 seconds before retrying again
|
43
|
+
config_set_default :max_retry_wait, 30 #wait a maximum of 30 seconds per retry
|
44
|
+
config_set_default :flush_interval, 5 #default flush interval of 5 seconds
|
45
|
+
|
46
|
+
def configure( conf )
|
47
|
+
#need to call this before super because there doesn't seem to be any other way to
|
48
|
+
#set the default value for the buffer_chunk_limit, which is created and configured in super
|
49
|
+
if !conf.key? "buffer_chunk_limit"
|
50
|
+
conf["buffer_chunk_limit"] = "100k"
|
51
|
+
end
|
52
|
+
if !conf.key? "buffer_queue_limit"
|
53
|
+
conf["buffer_queue_limit"] = 1024
|
54
|
+
end
|
55
|
+
super
|
56
|
+
|
57
|
+
if @buffer.buffer_chunk_limit > 1024*1024
|
58
|
+
$log.warn "Buffer chunk size is greater than 1Mb. This may result in requests being rejected by Scalyr"
|
59
|
+
end
|
60
|
+
|
61
|
+
if @max_request_buffer > (1024*1024*3)
|
62
|
+
$log.warn "Maximum request buffer > 3Mb. This may result in requests being rejected by Scalyr"
|
63
|
+
end
|
64
|
+
|
65
|
+
@message_encoding = nil
|
66
|
+
if @force_message_encoding.to_s != ''
|
67
|
+
begin
|
68
|
+
@message_encoding = Encoding.find( @force_message_encoding )
|
69
|
+
$log.debug "Forcing message encoding to '#{@force_message_encoding}'"
|
70
|
+
rescue ArgumentError
|
71
|
+
$log.warn "No encoding '#{@force_message_encoding}' found. Ignoring"
|
72
|
+
end
|
73
|
+
end
|
74
|
+
|
75
|
+
@scalyr_server << '/' unless @scalyr_server.end_with?('/')
|
76
|
+
|
77
|
+
@add_events_uri = URI @scalyr_server + "addEvents"
|
78
|
+
|
79
|
+
#forcibly limit the number of threads to 1 for now, to ensure requests always have incrementing timestamps
|
80
|
+
raise Fluent::ConfigError, "num_threads is currently limited to 1. You specified #{@num_threads}." if @num_threads > 1
|
81
|
+
end
|
82
|
+
|
83
|
+
def start
|
84
|
+
super
|
85
|
+
$log.info "Scalyr Fluentd Plugin ID - #{self.plugin_id()}"
|
86
|
+
#Generate a session id. This will be called once for each <match> in fluent.conf that uses scalyr
|
87
|
+
@session = SecureRandom.uuid
|
88
|
+
|
89
|
+
@sync = Mutex.new
|
90
|
+
#the following variables are all under the control of the above mutex
|
91
|
+
@thread_ids = Hash.new #hash of tags -> id
|
92
|
+
@next_id = 1 #incrementing thread id for the session
|
93
|
+
@last_timestamp = 0 #timestamp of most recent event
|
94
|
+
|
95
|
+
end
|
96
|
+
|
97
|
+
def format( tag, time, record )
|
98
|
+
begin
|
99
|
+
if @message_field != "message"
|
100
|
+
if record.key? @message_field
|
101
|
+
if record.key? "message"
|
102
|
+
$log.warn "Overwriting log record field 'message'. You are seeing this warning because in your fluentd config file you have configured the '#{@message_field}' field to be converted to the 'message' field, but the log record already contains a field called 'message' and this is now being overwritten."
|
103
|
+
end
|
104
|
+
record["message"] = record[@message_field]
|
105
|
+
record.delete( @message_field )
|
106
|
+
end
|
107
|
+
end
|
108
|
+
|
109
|
+
if @message_encoding
|
110
|
+
if @replace_invalid_utf8 and @message_encoding == Encoding::UTF_8
|
111
|
+
record["message"] = record["message"].encode("UTF-8", :invalid => :replace, :undef => :replace, :replace => "<?>").force_encoding('UTF-8')
|
112
|
+
else
|
113
|
+
record["message"].force_encoding( @message_encoding )
|
114
|
+
end
|
115
|
+
end
|
116
|
+
[tag, time, record].to_msgpack
|
117
|
+
|
118
|
+
rescue JSON::GeneratorError
|
119
|
+
$log.warn "Unable to format message due to JSON::GeneratorError. Record is:\n\t#{record.to_s}"
|
120
|
+
raise
|
121
|
+
end
|
122
|
+
end
|
123
|
+
|
124
|
+
#called by fluentd when a chunk of log messages is ready
|
125
|
+
def write( chunk )
|
126
|
+
begin
|
127
|
+
$log.debug "Size of chunk is: #{chunk.size}"
|
128
|
+
requests = self.build_add_events_body( chunk )
|
129
|
+
$log.debug "Chunk split into #{requests.size} request(s)."
|
130
|
+
|
131
|
+
requests.each_with_index { |request, index|
|
132
|
+
$log.debug "Request #{index + 1}/#{requests.size}: #{request[:body].bytesize} bytes"
|
133
|
+
begin
|
134
|
+
response = self.post_request( @add_events_uri, request[:body] )
|
135
|
+
self.handle_response( response )
|
136
|
+
rescue OpenSSL::SSL::SSLError => e
|
137
|
+
if e.message.include? "certificate verify failed"
|
138
|
+
$log.warn "SSL certificate verification failed. Please make sure your certificate bundle is configured correctly and points to a valid file. You can configure this with the ssl_ca_bundle_path configuration option. The current value of ssl_ca_bundle_path is '#{@ssl_ca_bundle_path}'"
|
139
|
+
end
|
140
|
+
$log.warn e.message
|
141
|
+
$log.warn "Discarding buffer chunk without retrying or logging to <secondary>"
|
142
|
+
rescue Scalyr::Client4xxError => e
|
143
|
+
$log.warn "4XX status code received for request #{index + 1}/#{requests.size}. Discarding buffer without retrying or logging.\n\t#{response.code} - #{e.message}\n\tChunk Size: #{chunk.size}\n\tLog messages this request: #{request[:record_count]}\n\tJSON payload size: #{request[:body].bytesize}\n\tSample: #{request[:body][0,1024]}..."
|
144
|
+
|
145
|
+
end
|
146
|
+
}
|
147
|
+
|
148
|
+
rescue JSON::GeneratorError
|
149
|
+
$log.warn "Unable to format message due to JSON::GeneratorError."
|
150
|
+
raise
|
151
|
+
end
|
152
|
+
end
|
153
|
+
|
154
|
+
|
155
|
+
|
156
|
+
#explicit function to convert to nanoseconds
|
157
|
+
#will make things easier to maintain if/when fluentd supports higher than second resolutions
|
158
|
+
def to_nanos( seconds )
|
159
|
+
seconds * 10**9
|
160
|
+
end
|
161
|
+
|
162
|
+
#explicit function to convert to milliseconds
|
163
|
+
#will make things easier to maintain if/when fluentd supports higher than second resolutions
|
164
|
+
def to_millis( seconds )
|
165
|
+
seconds * 10**6
|
166
|
+
end
|
167
|
+
|
168
|
+
def post_request( uri, body )
|
169
|
+
|
170
|
+
https = Net::HTTP.new( uri.host, uri.port )
|
171
|
+
https.use_ssl = true
|
172
|
+
|
173
|
+
#verify peers to prevent potential MITM attacks
|
174
|
+
if @ssl_verify_peer
|
175
|
+
https.ca_file = @ssl_ca_bundle_path
|
176
|
+
https.verify_mode = OpenSSL::SSL::VERIFY_PEER
|
177
|
+
https.verify_depth = @ssl_verify_depth
|
178
|
+
end
|
179
|
+
|
180
|
+
post = Net::HTTP::Post.new uri.path
|
181
|
+
post.add_field( 'Content-Type', 'application/json' )
|
182
|
+
|
183
|
+
post.body = body
|
184
|
+
|
185
|
+
https.request( post )
|
186
|
+
|
187
|
+
end
|
188
|
+
|
189
|
+
def handle_response( response )
|
190
|
+
$log.debug "Response Code: #{response.code}"
|
191
|
+
$log.debug "Response Body: #{response.body}"
|
192
|
+
|
193
|
+
response_hash = Hash.new
|
194
|
+
|
195
|
+
begin
|
196
|
+
response_hash = JSON.parse( response.body )
|
197
|
+
rescue
|
198
|
+
response_hash["status"] = "Invalid JSON response from server"
|
199
|
+
end
|
200
|
+
|
201
|
+
#make sure the JSON reponse has a "status" field
|
202
|
+
if !response_hash.key? "status"
|
203
|
+
$log.debug "JSON response does not contain status message"
|
204
|
+
raise Scalyr::ServerError.new "JSON response does not contain status message"
|
205
|
+
end
|
206
|
+
|
207
|
+
status = response_hash["status"]
|
208
|
+
|
209
|
+
#4xx codes are handled separately
|
210
|
+
if response.code =~ /^4\d\d/
|
211
|
+
raise Scalyr::Client4xxError.new status
|
212
|
+
else
|
213
|
+
if status != "success"
|
214
|
+
if status =~ /discardBuffer/
|
215
|
+
$log.warn "Received 'discardBuffer' message from server. Buffer dropped."
|
216
|
+
elsif status =~ %r"/client/"i
|
217
|
+
raise Scalyr::ClientError.new status
|
218
|
+
else #don't check specifically for server, we assume all non-client errors are server errors
|
219
|
+
raise Scalyr::ServerError.new status
|
220
|
+
end
|
221
|
+
elsif !response.code.include? "200" #response code is a string not an int
|
222
|
+
raise Scalyr::ServerError
|
223
|
+
end
|
224
|
+
end
|
225
|
+
|
226
|
+
end
|
227
|
+
|
228
|
+
def build_add_events_body( chunk )
|
229
|
+
|
230
|
+
#requests
|
231
|
+
requests = Array.new
|
232
|
+
|
233
|
+
#set of unique scalyr threads for this chunk
|
234
|
+
current_threads = Hash.new
|
235
|
+
|
236
|
+
#byte count
|
237
|
+
total_bytes = 0
|
238
|
+
|
239
|
+
#create a Scalyr event object for each record in the chunk
|
240
|
+
events = Array.new
|
241
|
+
chunk.msgpack_each {|(tag,time,record)|
|
242
|
+
|
243
|
+
timestamp = self.to_nanos( time )
|
244
|
+
thread_id = 0
|
245
|
+
|
246
|
+
@sync.synchronize {
|
247
|
+
#ensure timestamp is at least 1 nanosecond greater than the last one
|
248
|
+
timestamp = [timestamp, @last_timestamp + 1].max
|
249
|
+
@last_timestamp = timestamp
|
250
|
+
|
251
|
+
#get thread id or add a new one if we haven't seen this tag before
|
252
|
+
if @thread_ids.key? tag
|
253
|
+
thread_id = @thread_ids[tag]
|
254
|
+
else
|
255
|
+
thread_id = @next_id
|
256
|
+
@thread_ids[tag] = thread_id
|
257
|
+
@next_id += 1
|
258
|
+
end
|
259
|
+
}
|
260
|
+
|
261
|
+
#then update the map of threads for this chunk
|
262
|
+
current_threads[tag] = thread_id
|
263
|
+
|
264
|
+
#add a logfile field if one doesn't exist
|
265
|
+
if !record.key? "logfile"
|
266
|
+
record["logfile"] = "/fluentd/#{tag}"
|
267
|
+
end
|
268
|
+
|
269
|
+
#append to list of events
|
270
|
+
event = { :thread => thread_id.to_s,
|
271
|
+
:ts => timestamp.to_s,
|
272
|
+
:attrs => record
|
273
|
+
}
|
274
|
+
|
275
|
+
#get json string of event to keep track of how many bytes we are sending
|
276
|
+
|
277
|
+
begin
|
278
|
+
event_json = event.to_json
|
279
|
+
rescue JSON::GeneratorError, Encoding::UndefinedConversionError => e
|
280
|
+
$log.warn "#{e.class}: #{e.message}"
|
281
|
+
|
282
|
+
# Send the faulty event to a label @ERROR block and allow to handle it there (output to exceptions file for ex)
|
283
|
+
router.emit_error_event(tag, time, record, e)
|
284
|
+
|
285
|
+
event[:attrs].each do |key, value|
|
286
|
+
$log.debug "\t#{key} (#{value.encoding.name}): '#{value}'"
|
287
|
+
event[:attrs][key] = value.encode("UTF-8", :invalid => :replace, :undef => :replace, :replace => "<?>").force_encoding('UTF-8')
|
288
|
+
end
|
289
|
+
event_json = event.to_json
|
290
|
+
end
|
291
|
+
|
292
|
+
#generate new request if json size of events in the array exceed maximum request buffer size
|
293
|
+
append_event = true
|
294
|
+
if total_bytes + event_json.bytesize > @max_request_buffer
|
295
|
+
#make sure we always have at least one event
|
296
|
+
if events.size == 0
|
297
|
+
events << event
|
298
|
+
append_event = false
|
299
|
+
end
|
300
|
+
request = self.create_request( events, current_threads )
|
301
|
+
requests << request
|
302
|
+
|
303
|
+
total_bytes = 0
|
304
|
+
current_threads = Hash.new
|
305
|
+
events = Array.new
|
306
|
+
end
|
307
|
+
|
308
|
+
#if we haven't consumed the current event already
|
309
|
+
#add it to the end of our array and keep track of the json bytesize
|
310
|
+
if append_event
|
311
|
+
events << event
|
312
|
+
total_bytes += event_json.bytesize
|
313
|
+
end
|
314
|
+
|
315
|
+
}
|
316
|
+
|
317
|
+
#create a final request with any left over events
|
318
|
+
request = self.create_request( events, current_threads )
|
319
|
+
requests << request
|
320
|
+
|
321
|
+
end
|
322
|
+
|
323
|
+
def create_request( events, current_threads )
|
324
|
+
#build the scalyr thread objects
|
325
|
+
threads = Array.new
|
326
|
+
current_threads.each do |tag, id|
|
327
|
+
threads << { :id => id.to_s,
|
328
|
+
:name => "Fluentd: #{tag}"
|
329
|
+
}
|
330
|
+
end
|
331
|
+
|
332
|
+
current_time = self.to_millis( Fluent::Engine.now )
|
333
|
+
|
334
|
+
body = { :token => @api_write_token,
|
335
|
+
:client_timestamp => current_time.to_s,
|
336
|
+
:session => @session,
|
337
|
+
:events => events,
|
338
|
+
:threads => threads
|
339
|
+
}
|
340
|
+
|
341
|
+
#add server_attributes hash if it exists
|
342
|
+
if @server_attributes
|
343
|
+
body[:sessionInfo] = @server_attributes
|
344
|
+
end
|
345
|
+
|
346
|
+
{ :body => body.to_json, :record_count => events.size }
|
347
|
+
end
|
348
|
+
|
349
|
+
end
|
350
|
+
end
|
@@ -0,0 +1,24 @@
|
|
1
|
+
#
|
2
|
+
# Scalyr Output Plugin for Fluentd
|
3
|
+
#
|
4
|
+
# Copyright (C) 2015 Scalyr, Inc.
|
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
|
+
|
19
|
+
|
20
|
+
module Scalyr
|
21
|
+
class ClientError < StandardError; end
|
22
|
+
class Client4xxError < StandardError; end
|
23
|
+
class ServerError < StandardError; end
|
24
|
+
end
|
data/test/helper.rb
ADDED
@@ -0,0 +1,36 @@
|
|
1
|
+
#
|
2
|
+
# Scalyr Output Plugin for Fluentd
|
3
|
+
#
|
4
|
+
# Copyright (C) 2015 Scalyr, Inc.
|
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
|
+
|
19
|
+
require 'fluent/test'
|
20
|
+
require 'fluent/plugin/out_scalyr'
|
21
|
+
|
22
|
+
module Scalyr
|
23
|
+
class ScalyrOutTest < Test::Unit::TestCase
|
24
|
+
def setup
|
25
|
+
Fluent::Test.setup
|
26
|
+
end
|
27
|
+
|
28
|
+
CONFIG = %[
|
29
|
+
api_write_token test_token
|
30
|
+
]
|
31
|
+
|
32
|
+
def create_driver( conf = CONFIG )
|
33
|
+
Fluent::Test::BufferedOutputTestDriver.new( Scalyr::ScalyrOut ).configure( conf )
|
34
|
+
end
|
35
|
+
end
|
36
|
+
end
|
data/test/test_config.rb
ADDED
@@ -0,0 +1,58 @@
|
|
1
|
+
#
|
2
|
+
# Scalyr Output Plugin for Fluentd
|
3
|
+
#
|
4
|
+
# Copyright (C) 2015 Scalyr, Inc.
|
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
|
+
|
19
|
+
require 'helper'
|
20
|
+
|
21
|
+
class ConfigTest < Scalyr::ScalyrOutTest
|
22
|
+
|
23
|
+
def test_default_params
|
24
|
+
d = create_driver
|
25
|
+
assert_nil( d.instance.server_attributes, "Default server_attributes not nil" )
|
26
|
+
assert( d.instance.ssl_verify_peer, "Default ssl_verify_peer should be true" )
|
27
|
+
|
28
|
+
#check default buffer limits because they are set outside of the config_set_default
|
29
|
+
assert_equal( 100*1024, d.instance.buffer.buffer_chunk_limit, "Buffer chunk limit should be 100k" )
|
30
|
+
assert_equal( 1024, d.instance.buffer.buffer_queue_limit, "Buffer queue limit should be 1024" )
|
31
|
+
end
|
32
|
+
|
33
|
+
def test_configure_ssl_verify_peer
|
34
|
+
d = create_driver CONFIG + 'ssl_verify_peer false'
|
35
|
+
assert( !d.instance.ssl_verify_peer, "Config failed to set ssl_verify_peer" )
|
36
|
+
end
|
37
|
+
|
38
|
+
def test_scalyr_server_adding_trailing_slash
|
39
|
+
d = create_driver CONFIG + 'scalyr_server http://www.example.com'
|
40
|
+
assert_equal( "http://www.example.com/", d.instance.scalyr_server, "Missing trailing slash for scalyr_server" )
|
41
|
+
end
|
42
|
+
|
43
|
+
def test_configure_ssl_ca_bundle_path
|
44
|
+
d = create_driver CONFIG + 'ssl_ca_bundle_path /test/ca-bundle.crt'
|
45
|
+
assert_equal( "/test/ca-bundle.crt", d.instance.ssl_ca_bundle_path, "Config failed to set ssl_ca_bundle_path" )
|
46
|
+
end
|
47
|
+
|
48
|
+
def test_configure_ssl_verify_depth
|
49
|
+
d = create_driver CONFIG + 'ssl_verify_depth 10'
|
50
|
+
assert_equal( 10, d.instance.ssl_verify_depth, "Config failed to set ssl_verify_depth" )
|
51
|
+
end
|
52
|
+
|
53
|
+
def test_configure_server_attributes
|
54
|
+
d = create_driver CONFIG + 'server_attributes { "test":"value" }'
|
55
|
+
assert_equal( "value", d.instance.server_attributes["test"], "Config failed to set server_attributes" )
|
56
|
+
end
|
57
|
+
end
|
58
|
+
|