logstash-output-charrington 0.1.1 → 0.2.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/lib/logstash/outputs/charrington.rb +48 -259
- data/lib/logstash/outputs/charrington/alter_table.rb +98 -0
- data/lib/logstash/outputs/charrington/create_table.rb +69 -0
- data/lib/logstash/outputs/charrington/insert.rb +141 -0
- data/lib/logstash/outputs/charrington/process.rb +62 -0
- data/lib/logstash/outputs/charrington/service.rb +12 -0
- data/lib/logstash/outputs/charrington/transform.rb +47 -0
- data/logstash-output-charrington.gemspec +4 -1
- data/spec/outputs/charrington_spec.rb +44 -11
- metadata +36 -2
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: d8166adfa6e084d6e0fd56921edbc8b71dae22e6e5e282c86f73753692f14dbf
|
4
|
+
data.tar.gz: 5b0286ebc0540a5152a11ffacd441d465c6cc44b7c67e66a4b08f5c825588dca
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: b977663c714b04890525419c98773e80f2e55993ff3cfc0e50ca3dee6d2b06e81a948a39d1e0004ba156ed219482592ddb0f604514310a95fd6e9ac66312db5d
|
7
|
+
data.tar.gz: 32a8eee42bf6a88c3c3a244805546edc2a5cbe67626bd5b12450c2c963b1967f7aea2821cfbd068478a71a2364f4ca24e1fc5759c76f7ebb6bdffb64c5011e43
|
@@ -7,33 +7,40 @@ require 'java'
|
|
7
7
|
require 'logstash-output-charrington_jars'
|
8
8
|
require 'json'
|
9
9
|
require 'bigdecimal'
|
10
|
+
require 'pry'
|
11
|
+
require File.join(File.dirname(__FILE__), "charrington/process")
|
12
|
+
require File.join(File.dirname(__FILE__), "charrington/transform")
|
13
|
+
require File.join(File.dirname(__FILE__), "charrington/insert")
|
10
14
|
|
11
15
|
# Write events to a SQL engine, using JDBC.
|
12
|
-
#
|
13
|
-
|
14
|
-
#
|
15
|
-
#
|
16
|
-
#
|
16
|
+
# It is upto the user of the plugin to correctly configure the plugin.
|
17
|
+
|
18
|
+
# This class is responsible for setting things up, creating the connection,
|
19
|
+
# and handling retries. Charrington::Insert is where the insert
|
20
|
+
# is attempted. If that fails, it will try to either
|
21
|
+
# create a table via Charrington::CreateTable
|
22
|
+
# or alter an existing one via Charrington::AlterTable
|
23
|
+
|
17
24
|
class LogStash::Outputs::Charrington < LogStash::Outputs::Base
|
18
25
|
concurrency :shared
|
19
26
|
|
20
27
|
STRFTIME_FMT = '%Y-%m-%d %T.%L'.freeze
|
21
28
|
|
22
|
-
RETRYABLE_SQLSTATE_CLASSES = [
|
23
|
-
|
24
|
-
|
25
|
-
|
26
|
-
|
27
|
-
|
28
|
-
|
29
|
-
|
30
|
-
|
31
|
-
|
32
|
-
|
33
|
-
|
34
|
-
|
35
|
-
|
36
|
-
].freeze
|
29
|
+
# RETRYABLE_SQLSTATE_CLASSES = [
|
30
|
+
# # Classes of retryable SQLSTATE codes
|
31
|
+
# # Not all in the class will be retryable. However, this is the best that
|
32
|
+
# # we've got right now.
|
33
|
+
# # If a custom state code is required, set it in retry_sql_states.
|
34
|
+
# '08', # Connection Exception
|
35
|
+
# '24', # Invalid Cursor State (Maybe retry-able in some circumstances)
|
36
|
+
# '25', # Invalid Transaction State
|
37
|
+
# '40', # Transaction Rollback
|
38
|
+
# '53', # Insufficient Resources
|
39
|
+
# '54', # Program Limit Exceeded (MAYBE)
|
40
|
+
# '55', # Object Not In Prerequisite State
|
41
|
+
# '57', # Operator Intervention
|
42
|
+
# '58', # System Error
|
43
|
+
# ].freeze
|
37
44
|
|
38
45
|
config_name 'charrington'
|
39
46
|
|
@@ -56,26 +63,12 @@ class LogStash::Outputs::Charrington < LogStash::Outputs::Base
|
|
56
63
|
# jdbc password - optional, maybe in the connection string
|
57
64
|
config :password, validate: :string, required: false
|
58
65
|
|
59
|
-
# [ "insert into table (message) values(?)", "%{message}" ]
|
60
|
-
config :statement, validate: :array, required: true
|
61
|
-
|
62
|
-
# If this is an unsafe statement, use event.sprintf
|
63
|
-
# This also has potential performance penalties due to having to create a
|
64
|
-
# new statement for each event, rather than adding to the batch and issuing
|
65
|
-
# multiple inserts in 1 go
|
66
|
-
config :unsafe_statement, validate: :boolean, default: false
|
67
|
-
|
68
66
|
# Number of connections in the pool to maintain
|
69
67
|
config :max_pool_size, validate: :number, default: 5
|
70
68
|
|
71
69
|
# Connection timeout
|
72
70
|
config :connection_timeout, validate: :number, default: 10000
|
73
71
|
|
74
|
-
# We buffer a certain number of events before flushing that out to SQL.
|
75
|
-
# This setting controls how many events will be buffered before sending a
|
76
|
-
# batch of events.
|
77
|
-
config :flush_size, validate: :number, default: 1000
|
78
|
-
|
79
72
|
# Set initial interval in seconds between retries. Doubled on each retry up to `retry_max_interval`
|
80
73
|
config :retry_initial_interval, validate: :number, default: 2
|
81
74
|
|
@@ -94,7 +87,6 @@ class LogStash::Outputs::Charrington < LogStash::Outputs::Base
|
|
94
87
|
# Maximum number of sequential failed attempts, before we stop retrying.
|
95
88
|
# If set to < 1, then it will infinitely retry.
|
96
89
|
# At the default values this is a little over 10 minutes
|
97
|
-
|
98
90
|
config :max_flush_exceptions, validate: :number, default: 10
|
99
91
|
|
100
92
|
config :max_repeat_exceptions, obsolete: 'This has been replaced by max_flush_exceptions - which behaves slightly differently. Please check the documentation.'
|
@@ -114,25 +106,23 @@ class LogStash::Outputs::Charrington < LogStash::Outputs::Base
|
|
114
106
|
@logger.info('JDBC - Starting up')
|
115
107
|
|
116
108
|
load_jar_files!
|
117
|
-
|
118
109
|
@stopping = Concurrent::AtomicBoolean.new(false)
|
119
110
|
|
120
|
-
@logger.warn('JDBC - Flush size is set to > 1000') if @flush_size > 1000
|
121
|
-
|
122
|
-
if @statement.empty?
|
123
|
-
@logger.error('JDBC - No statement provided. Configuration error.')
|
124
|
-
end
|
125
|
-
|
126
|
-
if !@unsafe_statement && @statement.length < 2
|
127
|
-
@logger.error("JDBC - Statement has no parameters. No events will be inserted into SQL as you're not passing any event data. Likely configuration error.")
|
128
|
-
end
|
129
|
-
|
130
111
|
setup_and_test_pool!
|
131
112
|
end
|
132
113
|
|
133
114
|
def multi_receive(events)
|
134
|
-
events.
|
135
|
-
|
115
|
+
events.each do |event|
|
116
|
+
connection = get_connection
|
117
|
+
break unless connection
|
118
|
+
|
119
|
+
opts = { connection: connection,
|
120
|
+
schema: @schema,
|
121
|
+
max_retries: @max_flush_exceptions,
|
122
|
+
retry_initial_interval: @retry_initial_interval }
|
123
|
+
|
124
|
+
Charrington::Process.call(connection, event, opts)
|
125
|
+
connection.close unless connection.nil?
|
136
126
|
end
|
137
127
|
end
|
138
128
|
|
@@ -145,19 +135,14 @@ class LogStash::Outputs::Charrington < LogStash::Outputs::Base
|
|
145
135
|
private
|
146
136
|
|
147
137
|
def setup_and_test_pool!
|
148
|
-
# Setup pool
|
149
138
|
@pool = Java::ComZaxxerHikari::HikariDataSource.new
|
150
|
-
|
151
|
-
@pool.setAutoCommit(@driver_auto_commit)
|
152
139
|
@pool.setDriverClassName(@driver_class) if @driver_class
|
153
|
-
|
154
|
-
@pool.setJdbcUrl(@connection_string)
|
155
|
-
|
156
140
|
@pool.setUsername(@username) if @username
|
157
141
|
@pool.setPassword(@password) if @password
|
158
|
-
|
159
142
|
@pool.setMaximumPoolSize(@max_pool_size)
|
160
143
|
@pool.setConnectionTimeout(@connection_timeout)
|
144
|
+
@pool.setAutoCommit(@driver_auto_commit)
|
145
|
+
@pool.setJdbcUrl(@connection_string)
|
161
146
|
|
162
147
|
validate_connection_timeout = (@connection_timeout / 1000) / 2
|
163
148
|
|
@@ -176,16 +161,15 @@ class LogStash::Outputs::Charrington < LogStash::Outputs::Base
|
|
176
161
|
test_connection.close
|
177
162
|
end
|
178
163
|
|
164
|
+
# Load jar from driver path
|
179
165
|
def load_jar_files!
|
180
|
-
# Load jar from driver path
|
181
166
|
unless @driver_jar_path.nil?
|
182
167
|
raise LogStash::ConfigurationError, 'JDBC - Could not find jar file at given path. Check config.' unless File.exist? @driver_jar_path
|
183
168
|
require @driver_jar_path
|
184
169
|
return
|
185
170
|
end
|
186
171
|
|
187
|
-
# Revert original behaviour of loading from vendor directory
|
188
|
-
# if no path given
|
172
|
+
# Revert original behaviour of loading from vendor directory if no path given
|
189
173
|
jarpath = if ENV['LOGSTASH_HOME']
|
190
174
|
File.join(ENV['LOGSTASH_HOME'], '/vendor/jar/jdbc/*.jar')
|
191
175
|
else
|
@@ -203,192 +187,11 @@ class LogStash::Outputs::Charrington < LogStash::Outputs::Base
|
|
203
187
|
end
|
204
188
|
end
|
205
189
|
|
206
|
-
def
|
207
|
-
|
208
|
-
|
209
|
-
|
210
|
-
|
211
|
-
|
212
|
-
value_placeholders = ('?' * hashed.length).split('')
|
213
|
-
values = '(' + value_placeholders.join(', ') + ')'
|
214
|
-
|
215
|
-
table_name = create_table_name(event)
|
216
|
-
return "INSERT INTO #{table_name} #{columns} VALUES #{values}", hashed.keys
|
217
|
-
end
|
218
|
-
|
219
|
-
def create_table_name(event)
|
220
|
-
raise TableNameNil.new("Table name is nil", event) if event.nil?
|
221
|
-
|
222
|
-
event = event.to_hash["event"].to_s.strip
|
223
|
-
raise TableNameNil.new("Table name is nil", event) if event.empty?
|
224
|
-
|
225
|
-
schema = @schema.empty? ? '' : "#{@schema}."
|
226
|
-
"#{schema}#{event.gsub(/[ \-_]+/, "_").downcase}"
|
227
|
-
end
|
228
|
-
|
229
|
-
def prepared_statement(keys)
|
230
|
-
keys.map do |key|
|
231
|
-
turn_into_wrapped(key)
|
232
|
-
end
|
233
|
-
end
|
234
|
-
|
235
|
-
def turn_into_wrapped(key)
|
236
|
-
"[#{key}]"
|
237
|
-
end
|
238
|
-
|
239
|
-
def submit(events)
|
240
|
-
connection = nil
|
241
|
-
statement = nil
|
242
|
-
events_to_retry = []
|
243
|
-
|
244
|
-
begin
|
245
|
-
connection = @pool.getConnection
|
246
|
-
rescue => e
|
247
|
-
log_jdbc_exception(e, true, nil)
|
248
|
-
# If a connection is not available, then the server has gone away
|
249
|
-
# We're not counting that towards our retry count.
|
250
|
-
return events, false
|
251
|
-
end
|
252
|
-
|
253
|
-
events.each do |event|
|
254
|
-
|
255
|
-
|
256
|
-
begin
|
257
|
-
ins, columns = create_statement(event)
|
258
|
-
keys_we_care_about = prepared_statement(columns)
|
259
|
-
statement = connection.prepareStatement(
|
260
|
-
ins
|
261
|
-
)
|
262
|
-
statement = add_statement_event_params(statement, event, keys_we_care_about)
|
263
|
-
statement.execute
|
264
|
-
rescue TableNameNil => e
|
265
|
-
@logger.error("#{e.message} event=#{e.event}")
|
266
|
-
rescue => e
|
267
|
-
@logger.error "Rescue from SQLException #{e.message}"
|
268
|
-
create_statement = make_create_statement(event, columns)
|
269
|
-
puts 'create_statement'
|
270
|
-
puts create_statement
|
271
|
-
|
272
|
-
statement = connection.prepareStatement(
|
273
|
-
create_statement
|
274
|
-
)
|
275
|
-
statement.execute
|
276
|
-
@logger.debug('Created new Table.')
|
277
|
-
events_to_retry.push(event)
|
278
|
-
ensure
|
279
|
-
statement.close unless statement.nil?
|
280
|
-
end
|
281
|
-
end
|
282
|
-
|
283
|
-
connection.close unless connection.nil?
|
284
|
-
|
285
|
-
return events_to_retry, true
|
286
|
-
end
|
287
|
-
|
288
|
-
def retrying_submit(actions)
|
289
|
-
# Initially we submit the full list of actions
|
290
|
-
submit_actions = actions
|
291
|
-
count_as_attempt = true
|
292
|
-
|
293
|
-
attempts = 1
|
294
|
-
|
295
|
-
sleep_interval = @retry_initial_interval
|
296
|
-
while @stopping.false? and (submit_actions and !submit_actions.empty?)
|
297
|
-
return if !submit_actions || submit_actions.empty? # If everything's a success we move along
|
298
|
-
# We retry whatever didn't succeed
|
299
|
-
submit_actions, count_as_attempt = submit(submit_actions)
|
300
|
-
|
301
|
-
# Everything was a success!
|
302
|
-
break if !submit_actions || submit_actions.empty?
|
303
|
-
|
304
|
-
if @max_flush_exceptions > 0 and count_as_attempt == true
|
305
|
-
attempts += 1
|
306
|
-
|
307
|
-
if attempts > @max_flush_exceptions
|
308
|
-
@logger.error("JDBC - max_flush_exceptions has been reached. #{submit_actions.length} events have been unable to be sent to SQL and are being dropped. See previously logged exceptions for details.")
|
309
|
-
break
|
310
|
-
end
|
311
|
-
end
|
312
|
-
|
313
|
-
# If we're retrying the action sleep for the recommended interval
|
314
|
-
# Double the interval for the next time through to achieve exponential backoff
|
315
|
-
Stud.stoppable_sleep(sleep_interval) { @stopping.true? }
|
316
|
-
sleep_interval = next_sleep_interval(sleep_interval)
|
317
|
-
end
|
318
|
-
end
|
319
|
-
|
320
|
-
def make_create_statement(event, keys_we_care_about)
|
321
|
-
columns = []
|
322
|
-
|
323
|
-
keys_we_care_about.each_with_index do |key, idx|
|
324
|
-
wrapped = turn_into_wrapped(key)
|
325
|
-
|
326
|
-
case event.get(wrapped)
|
327
|
-
when Time, LogStash::Timestamp
|
328
|
-
columns << "#{key} TIMESTAMP"
|
329
|
-
when Integer
|
330
|
-
columns << "#{key} BIGINT"
|
331
|
-
when BigDecimal
|
332
|
-
columns << "#{key} DECIMAL"
|
333
|
-
when Float
|
334
|
-
columns << "#{key} DOUBLE PRECISION"
|
335
|
-
when String, Array, Hash
|
336
|
-
columns << "#{key} VARCHAR"
|
337
|
-
when true, false
|
338
|
-
columns << "#{key} BOOLEAN"
|
339
|
-
end
|
340
|
-
end
|
341
|
-
|
342
|
-
"CREATE TABLE IF NOT EXISTS #{create_table_name(event)} (#{columns.join(', ')})"
|
343
|
-
end
|
344
|
-
|
345
|
-
def add_statement_event_params(statement, event, keys_we_care_about)
|
346
|
-
keys_we_care_about.each_with_index do |key, idx|
|
347
|
-
if @enable_event_as_json_keyword == true and key.is_a? String and key == @event_as_json_keyword
|
348
|
-
value = event.to_json
|
349
|
-
elsif key.is_a? String
|
350
|
-
value = event.get(key)
|
351
|
-
if value.nil? and key =~ /%\{/
|
352
|
-
value = event.sprintf(key)
|
353
|
-
end
|
354
|
-
else
|
355
|
-
value = key
|
356
|
-
end
|
357
|
-
|
358
|
-
case value
|
359
|
-
when Time
|
360
|
-
statement.setString(idx + 1, value.strftime(STRFTIME_FMT))
|
361
|
-
when LogStash::Timestamp
|
362
|
-
statement.setString(idx + 1, value.time.strftime(STRFTIME_FMT))
|
363
|
-
when Integer
|
364
|
-
if value > 2147483647 or value < -2147483648
|
365
|
-
statement.setLong(idx + 1, value)
|
366
|
-
else
|
367
|
-
statement.setInt(idx + 1, value)
|
368
|
-
end
|
369
|
-
when BigDecimal
|
370
|
-
statement.setBigDecimal(idx + 1, value.to_java)
|
371
|
-
when Float
|
372
|
-
statement.setFloat(idx + 1, value)
|
373
|
-
when String
|
374
|
-
statement.setString(idx + 1, value)
|
375
|
-
when Array, Hash
|
376
|
-
statement.setString(idx + 1, value.to_json)
|
377
|
-
when true, false
|
378
|
-
statement.setBoolean(idx + 1, value)
|
379
|
-
else
|
380
|
-
statement.setString(idx + 1, nil)
|
381
|
-
end
|
382
|
-
end
|
383
|
-
|
384
|
-
statement
|
385
|
-
end
|
386
|
-
|
387
|
-
def retry_exception?(exception, event)
|
388
|
-
retrying = (exception.respond_to? 'getSQLState' and (RETRYABLE_SQLSTATE_CLASSES.include?(exception.getSQLState.to_s[0,2]) or @retry_sql_states.include?(exception.getSQLState)))
|
389
|
-
log_jdbc_exception(exception, retrying, event)
|
390
|
-
|
391
|
-
retrying
|
190
|
+
def get_connection
|
191
|
+
connection = @pool.getConnection
|
192
|
+
rescue => e
|
193
|
+
log_jdbc_exception(e, true, nil)
|
194
|
+
false
|
392
195
|
end
|
393
196
|
|
394
197
|
def log_jdbc_exception(exception, retrying, event)
|
@@ -400,7 +203,7 @@ class LogStash::Outputs::Charrington < LogStash::Outputs::Base
|
|
400
203
|
loop do
|
401
204
|
# TODO reformat event output so that it only shows the fields necessary.
|
402
205
|
|
403
|
-
@logger.send(log_method, log_text, :exception => current_exception, :
|
206
|
+
@logger.send(log_method, log_text, :exception => current_exception, :event => event)
|
404
207
|
|
405
208
|
if current_exception.respond_to? 'getNextException'
|
406
209
|
current_exception = current_exception.getNextException()
|
@@ -411,18 +214,4 @@ class LogStash::Outputs::Charrington < LogStash::Outputs::Base
|
|
411
214
|
break if current_exception == nil
|
412
215
|
end
|
413
216
|
end
|
414
|
-
|
415
|
-
def next_sleep_interval(current_interval)
|
416
|
-
doubled = current_interval * 2
|
417
|
-
doubled > @retry_max_interval ? @retry_max_interval : doubled
|
418
|
-
end
|
419
|
-
end # class LogStash::Outputs::Charrington
|
420
|
-
|
421
|
-
class TableNameNil < StandardError
|
422
|
-
attr_reader :event
|
423
|
-
|
424
|
-
def initialize(msg='Table name is nil', event={})
|
425
|
-
@event = event
|
426
|
-
super(msg)
|
427
|
-
end
|
428
217
|
end
|
@@ -0,0 +1,98 @@
|
|
1
|
+
require File.join(File.dirname(__FILE__), "service")
|
2
|
+
|
3
|
+
module Charrington
|
4
|
+
class AlterTable
|
5
|
+
# This service will add columns to an existing table dynamically based on finding new keys in the JSON structure.
|
6
|
+
# This is potentially called from Insert when an insert fails.
|
7
|
+
|
8
|
+
include Service
|
9
|
+
attr_reader :connection, :event, :table_name, :columns
|
10
|
+
attr_accessor :column_types
|
11
|
+
|
12
|
+
Error = Class.new(StandardError)
|
13
|
+
AlterFailed = Class.new(Error)
|
14
|
+
|
15
|
+
def initialize(connection, event, table_name, columns)
|
16
|
+
@connection = connection
|
17
|
+
@event = event
|
18
|
+
@table_name = table_name
|
19
|
+
@columns = columns
|
20
|
+
@column_types = []
|
21
|
+
end
|
22
|
+
|
23
|
+
def call
|
24
|
+
set_column_types
|
25
|
+
alter_table
|
26
|
+
true
|
27
|
+
rescue => e
|
28
|
+
raise AlterFailed, e.message
|
29
|
+
ensure
|
30
|
+
@column_types.clear if @column_types.is_a? Array
|
31
|
+
end
|
32
|
+
|
33
|
+
private
|
34
|
+
|
35
|
+
def alter_table
|
36
|
+
execute("ALTER TABLE IF EXISTS #{table_name} #{columns_fragment}")
|
37
|
+
end
|
38
|
+
|
39
|
+
def columns_fragment
|
40
|
+
column_types.map do |column|
|
41
|
+
"ADD COLUMN IF NOT EXISTS #{column}"
|
42
|
+
end.join(",")
|
43
|
+
end
|
44
|
+
|
45
|
+
def set_column_types
|
46
|
+
(columns - current_table_columns).each_with_index do |key, idx|
|
47
|
+
|
48
|
+
case event[key]
|
49
|
+
when Time, LogStash::Timestamp
|
50
|
+
column_types << "#{key} TIMESTAMP"
|
51
|
+
when Date
|
52
|
+
column_types << "#{key} DATE"
|
53
|
+
when Integer
|
54
|
+
column_types << "#{key} BIGINT"
|
55
|
+
when BigDecimal
|
56
|
+
column_types << "#{key} DECIMAL"
|
57
|
+
when Float
|
58
|
+
column_types << "#{key} DOUBLE PRECISION"
|
59
|
+
when true, false
|
60
|
+
column_types << "#{key} BOOLEAN"
|
61
|
+
else
|
62
|
+
column_types << "#{key} VARCHAR"
|
63
|
+
end
|
64
|
+
end
|
65
|
+
end
|
66
|
+
|
67
|
+
def current_table_columns
|
68
|
+
sql = "SELECT * FROM #{table_name} LIMIT 1;"
|
69
|
+
rs = executeQuery(sql)
|
70
|
+
meta_data = rs.getMetaData()
|
71
|
+
column_count = meta_data.getColumnCount()
|
72
|
+
|
73
|
+
(1..column_count).map {|i| meta_data.getColumnName(i) }
|
74
|
+
end
|
75
|
+
|
76
|
+
def execute(sql)
|
77
|
+
stmt = connection.prepareStatement(prep_sql(sql))
|
78
|
+
stmt.execute()
|
79
|
+
rescue Java::OrgPostgresqlUtil::PSQLException => e
|
80
|
+
# @logger.error("#{e.message}")
|
81
|
+
ensure
|
82
|
+
stmt.close unless stmt.nil?
|
83
|
+
end
|
84
|
+
|
85
|
+
def executeQuery(sql)
|
86
|
+
stmt = connection.createStatement()
|
87
|
+
stmt.executeQuery(prep_sql(sql))
|
88
|
+
rescue Java::OrgPostgresqlUtil::PSQLException => e
|
89
|
+
# @logger.error("#{e.message}")
|
90
|
+
ensure
|
91
|
+
stmt.close unless stmt.nil?
|
92
|
+
end
|
93
|
+
|
94
|
+
def prep_sql(sql)
|
95
|
+
sql.gsub(/\s+/, " ").strip
|
96
|
+
end
|
97
|
+
end
|
98
|
+
end
|
@@ -0,0 +1,69 @@
|
|
1
|
+
require File.join(File.dirname(__FILE__), "service")
|
2
|
+
|
3
|
+
module Charrington
|
4
|
+
class CreateTable
|
5
|
+
# This service will create a table dynamically based on the JSON structure.
|
6
|
+
# This is potentially called from Insert when an insert fails.
|
7
|
+
|
8
|
+
include Service
|
9
|
+
attr_reader :connection, :event, :table_name, :columns
|
10
|
+
attr_accessor :column_types
|
11
|
+
|
12
|
+
Error = Class.new(StandardError)
|
13
|
+
CreateFailed = Class.new(Error)
|
14
|
+
|
15
|
+
def initialize(connection, event, table_name, columns)
|
16
|
+
@connection = connection
|
17
|
+
@event = event.to_hash
|
18
|
+
@table_name = table_name
|
19
|
+
@columns = columns
|
20
|
+
@column_types = []
|
21
|
+
end
|
22
|
+
|
23
|
+
def call
|
24
|
+
set_column_types
|
25
|
+
create_table
|
26
|
+
true
|
27
|
+
rescue => e
|
28
|
+
raise CreateFailed, e.message
|
29
|
+
ensure
|
30
|
+
@column_types.clear if @column_types.is_a? Array
|
31
|
+
end
|
32
|
+
|
33
|
+
private
|
34
|
+
|
35
|
+
def set_column_types
|
36
|
+
columns.each do |column|
|
37
|
+
case event[column]
|
38
|
+
when Time, LogStash::Timestamp
|
39
|
+
column_types << "#{column} TIMESTAMP"
|
40
|
+
when Date
|
41
|
+
column_types << "#{column} DATE"
|
42
|
+
when Integer
|
43
|
+
column_types << "#{column} BIGINT"
|
44
|
+
when BigDecimal
|
45
|
+
column_types << "#{column} DECIMAL"
|
46
|
+
when Float
|
47
|
+
column_types << "#{column} DOUBLE PRECISION"
|
48
|
+
when true, false
|
49
|
+
column_types << "#{column} BOOLEAN"
|
50
|
+
else
|
51
|
+
column_types << "#{column} VARCHAR"
|
52
|
+
end
|
53
|
+
end
|
54
|
+
end
|
55
|
+
|
56
|
+
def create_table
|
57
|
+
execute("CREATE TABLE IF NOT EXISTS #{table_name} (#{column_types.join(', ')})")
|
58
|
+
end
|
59
|
+
|
60
|
+
def execute(sql)
|
61
|
+
statement = connection.prepareStatement( sql.gsub(/\s+/, " ").strip )
|
62
|
+
statement.execute()
|
63
|
+
rescue Java::OrgPostgresqlUtil::PSQLException => e
|
64
|
+
# @logger.error("#{e.message}")
|
65
|
+
ensure
|
66
|
+
statement.close unless statement.nil?
|
67
|
+
end
|
68
|
+
end
|
69
|
+
end
|
@@ -0,0 +1,141 @@
|
|
1
|
+
require File.join(File.dirname(__FILE__), "create_table")
|
2
|
+
require File.join(File.dirname(__FILE__), "alter_table")
|
3
|
+
require File.join(File.dirname(__FILE__), "service")
|
4
|
+
|
5
|
+
module Charrington
|
6
|
+
class Insert
|
7
|
+
# This service assumes that the data is already clean and in a flattened hash format.
|
8
|
+
# The Transform service should be called before calling this.
|
9
|
+
|
10
|
+
include Service
|
11
|
+
attr_accessor :event, :should_retry
|
12
|
+
attr_reader :connection, :schema, :table_name, :columns
|
13
|
+
attr_reader :event_as_json_keyword, :enable_event_as_json_keyword
|
14
|
+
|
15
|
+
Error = Class.new(StandardError)
|
16
|
+
EventNil = Class.new(Error)
|
17
|
+
TableNameNil = Class.new(Error)
|
18
|
+
InsertFailed = Class.new(Error)
|
19
|
+
|
20
|
+
def initialize(connection, event, opts = {})
|
21
|
+
raise EventNil, "Table name is nil" if event.nil?
|
22
|
+
@event = event.to_hash
|
23
|
+
|
24
|
+
event_name = event["event"].to_s.downcase.strip
|
25
|
+
raise TableNameNil, "Table name is nil" if event_name.empty?
|
26
|
+
|
27
|
+
@connection = connection
|
28
|
+
@schema = opts[:schema].empty? ? '' : "#{opts[:schema]}."
|
29
|
+
@table_name = "#{@schema}#{event_name.gsub(/[^a-z0-9]+/, "_")}"
|
30
|
+
|
31
|
+
@columns = event.keys
|
32
|
+
@should_retry = false
|
33
|
+
@enable_event_as_json_keyword = opts[:enable_event_as_json_keyword]
|
34
|
+
@event_as_json_keyword = opts[:event_as_json_keyword]
|
35
|
+
end
|
36
|
+
|
37
|
+
def call
|
38
|
+
stmt = connection.prepareStatement(insert_statement)
|
39
|
+
stmt = add_statement_event_params(stmt)
|
40
|
+
stmt.execute
|
41
|
+
should_retry
|
42
|
+
rescue Java::OrgPostgresqlUtil::PSQLException => e
|
43
|
+
case e.getSQLState()
|
44
|
+
when "42P01"
|
45
|
+
should_retry = Charrington::CreateTable.call(connection, event, table_name, columns)
|
46
|
+
when "42703"
|
47
|
+
should_retry = Charrington::AlterTable.call(connection, event, table_name, columns)
|
48
|
+
else
|
49
|
+
raise InsertFailed, "Charrington: Rescue from SQLException #{e.message}"
|
50
|
+
end
|
51
|
+
should_retry
|
52
|
+
rescue => e
|
53
|
+
raise InsertFailed, "Charrington: Rescue from SQLException #{e.message}"
|
54
|
+
ensure
|
55
|
+
stmt.close unless stmt.nil?
|
56
|
+
cleanup
|
57
|
+
end
|
58
|
+
|
59
|
+
private
|
60
|
+
|
61
|
+
def cleanup
|
62
|
+
@columns.clear if clearable(@columns)
|
63
|
+
end
|
64
|
+
|
65
|
+
### Set Variables
|
66
|
+
|
67
|
+
def columns_text
|
68
|
+
@columns_text ||= arr_to_csv(columns)
|
69
|
+
end
|
70
|
+
|
71
|
+
def value_placeholders
|
72
|
+
('?' * columns.length).split('')
|
73
|
+
end
|
74
|
+
|
75
|
+
def insert_values
|
76
|
+
arr_to_csv(value_placeholders)
|
77
|
+
end
|
78
|
+
|
79
|
+
def insert_statement
|
80
|
+
"INSERT INTO #{table_name} #{columns_text} VALUES #{insert_values}"
|
81
|
+
end
|
82
|
+
|
83
|
+
def prepared_statement
|
84
|
+
columns.map { |column| "[#{column}]" }
|
85
|
+
end
|
86
|
+
|
87
|
+
def add_statement_event_params(stmt)
|
88
|
+
columns.each_with_index do |key, idx|
|
89
|
+
pos = idx + 1
|
90
|
+
value = event[key]
|
91
|
+
|
92
|
+
case value
|
93
|
+
when Time
|
94
|
+
stmt.setString(pos, value.strftime(STRFTIME_FMT))
|
95
|
+
when LogStash::Timestamp
|
96
|
+
stmt.setString(pos, value.time.strftime(STRFTIME_FMT))
|
97
|
+
when Integer
|
98
|
+
if value > 2147483647 || value < -2147483648
|
99
|
+
stmt.setLong(pos, value)
|
100
|
+
else
|
101
|
+
stmt.setInt(pos, value)
|
102
|
+
end
|
103
|
+
when BigDecimal
|
104
|
+
stmt.setBigDecimal(pos, value.to_java)
|
105
|
+
when Float
|
106
|
+
stmt.setFloat(pos, value)
|
107
|
+
when String
|
108
|
+
stmt.setString(pos, value)
|
109
|
+
when Array, Hash
|
110
|
+
stmt.setString(pos, value.to_json)
|
111
|
+
when true, false
|
112
|
+
stmt.setBoolean(pos, value)
|
113
|
+
else
|
114
|
+
stmt.setString(pos, nil)
|
115
|
+
end
|
116
|
+
end
|
117
|
+
stmt
|
118
|
+
end
|
119
|
+
|
120
|
+
### Helpers
|
121
|
+
|
122
|
+
def arr_to_csv(arr)
|
123
|
+
'(' + arr.join(', ') + ')'
|
124
|
+
end
|
125
|
+
|
126
|
+
def clearable(obj)
|
127
|
+
obj.is_a? Hash or obj.is_a? Array
|
128
|
+
end
|
129
|
+
|
130
|
+
### SQL
|
131
|
+
|
132
|
+
def execute(connection, sql)
|
133
|
+
statement = connection.prepareStatement( sql.gsub(/\s+/, " ").strip )
|
134
|
+
statement.execute()
|
135
|
+
rescue Java::OrgPostgresqlUtil::PSQLException => e
|
136
|
+
@logger.error("#{e.message}")
|
137
|
+
ensure
|
138
|
+
statement.close unless statement.nil?
|
139
|
+
end
|
140
|
+
end
|
141
|
+
end
|
@@ -0,0 +1,62 @@
|
|
1
|
+
require File.join(File.dirname(__FILE__), "service")
|
2
|
+
|
3
|
+
module Charrington
|
4
|
+
class Process
|
5
|
+
# This service starts the process of attempting to insert a row.
|
6
|
+
# It handles retries where applicable.
|
7
|
+
|
8
|
+
include Service
|
9
|
+
attr_reader :event, :connection, :opts, :max_retries, :schema, :retry_max_interval
|
10
|
+
attr_accessor :retry_interval, :should_retry
|
11
|
+
|
12
|
+
Error = Class.new(StandardError)
|
13
|
+
ProcessFailed = Class.new(Error)
|
14
|
+
EventNil = Class.new(Error)
|
15
|
+
|
16
|
+
def initialize(connection, event, opts={})
|
17
|
+
raise EventNil, "Event is nil" if event.nil?
|
18
|
+
@connection = connection
|
19
|
+
@event = event.to_hash
|
20
|
+
@opts = opts
|
21
|
+
|
22
|
+
@max_retries = opts[:max_retries] || 10
|
23
|
+
@retry_max_interval = opts[:retry_max_interval] || 2
|
24
|
+
@retry_interval = opts[:retry_initial_interval] || 2
|
25
|
+
|
26
|
+
@attempts = 1
|
27
|
+
@should_retry = true
|
28
|
+
end
|
29
|
+
|
30
|
+
def call
|
31
|
+
while should_retry do
|
32
|
+
transformed = Charrington::Transform.call(event)
|
33
|
+
should_retry = Charrington::Insert.call(connection, transformed, opts)
|
34
|
+
break if !should_retry
|
35
|
+
|
36
|
+
@attempts += 1
|
37
|
+
break if @attempts > max_retries
|
38
|
+
|
39
|
+
# If we're retrying the action, sleep for the recommended interval
|
40
|
+
# Double the interval for the next time through to achieve exponential backoff
|
41
|
+
sleep_interval
|
42
|
+
end
|
43
|
+
rescue => e
|
44
|
+
raise ProcessFailed, e.message
|
45
|
+
ensure
|
46
|
+
connection.close unless connection.nil?
|
47
|
+
@event.clear if clearable(@event)
|
48
|
+
end
|
49
|
+
|
50
|
+
private
|
51
|
+
|
52
|
+
def sleep_interval
|
53
|
+
sleep(retry_interval)
|
54
|
+
doubled = retry_interval * 2
|
55
|
+
retry_interval = doubled > retry_max_interval ? retry_max_interval : doubled
|
56
|
+
end
|
57
|
+
|
58
|
+
def clearable(obj)
|
59
|
+
obj.is_a? Hash or obj.is_a? Array
|
60
|
+
end
|
61
|
+
end
|
62
|
+
end
|
@@ -0,0 +1,47 @@
|
|
1
|
+
require File.join(File.dirname(__FILE__), "service")
|
2
|
+
|
3
|
+
module Charrington
|
4
|
+
class Transform
|
5
|
+
include Service
|
6
|
+
attr_accessor :event
|
7
|
+
attr_reader :top_level_keys
|
8
|
+
|
9
|
+
Error = Class.new(StandardError)
|
10
|
+
EventNil = Class.new(Error)
|
11
|
+
TableNameNil = Class.new(Error)
|
12
|
+
|
13
|
+
KEY_BLACKLIST = ['host','path','jwt']
|
14
|
+
|
15
|
+
def initialize(event)
|
16
|
+
raise EventNil, "Event is nil" if event.nil?
|
17
|
+
event = event.is_a?(Hash) ? event : event.to_hash
|
18
|
+
@event = drop_keys(event)
|
19
|
+
@top_level_keys = @event.keys
|
20
|
+
end
|
21
|
+
|
22
|
+
def call
|
23
|
+
flattened = flatten_hash(event)
|
24
|
+
top_level_keys.each { |k| event.delete(k) }
|
25
|
+
flattened.each_pair { |key, val| event[key] = val }
|
26
|
+
event
|
27
|
+
end
|
28
|
+
|
29
|
+
private
|
30
|
+
|
31
|
+
def drop_keys(event)
|
32
|
+
event.delete_if {|k, _v| k.start_with?("@") || KEY_BLACKLIST.include?(k) }
|
33
|
+
end
|
34
|
+
|
35
|
+
def flatten_hash(hash)
|
36
|
+
hash.each_with_object({}) do |(k, v), acc|
|
37
|
+
if v.is_a? Hash
|
38
|
+
flatten_hash(v).map do |h_k, h_v|
|
39
|
+
acc["#{k}_#{h_k}"] = h_v
|
40
|
+
end
|
41
|
+
else
|
42
|
+
acc[k] = v
|
43
|
+
end
|
44
|
+
end
|
45
|
+
end
|
46
|
+
end
|
47
|
+
end
|
@@ -1,6 +1,7 @@
|
|
1
1
|
Gem::Specification.new do |s|
|
2
2
|
s.name = 'logstash-output-charrington'
|
3
|
-
s.version = '0.
|
3
|
+
s.version = '0.2.0'
|
4
|
+
|
4
5
|
s.licenses = ['Apache License (2.0)']
|
5
6
|
s.summary = 'This plugin allows you to output to SQL, via JDBC'
|
6
7
|
s.description = "This gem is a logstash plugin required to be installed on top of the Logstash core pipeline using $LS_HOME/bin/logstash-plugin install 'logstash-output-charrington'. This gem is not a stand-alone program"
|
@@ -27,4 +28,6 @@ Gem::Specification.new do |s|
|
|
27
28
|
s.add_development_dependency 'jar-dependencies'
|
28
29
|
s.add_development_dependency 'ruby-maven', '~> 3.3'
|
29
30
|
s.add_development_dependency 'rubocop', '0.41.2'
|
31
|
+
s.add_development_dependency 'logstash-input-generator'
|
32
|
+
s.add_development_dependency 'logstash-codec-json'
|
30
33
|
end
|
@@ -1,11 +1,44 @@
|
|
1
|
-
|
2
|
-
|
3
|
-
|
4
|
-
|
5
|
-
|
6
|
-
|
7
|
-
|
8
|
-
|
9
|
-
|
10
|
-
|
11
|
-
|
1
|
+
require_relative '../charrington_spec_helper'
|
2
|
+
|
3
|
+
describe LogStash::Outputs::Charrington do
|
4
|
+
describe 'when initializing' do
|
5
|
+
it 'shouldn\'t register without a config' do
|
6
|
+
expect do
|
7
|
+
LogStash::Plugin.lookup('output', 'charrington').new
|
8
|
+
end.to raise_error(LogStash::ConfigurationError)
|
9
|
+
end
|
10
|
+
end
|
11
|
+
|
12
|
+
describe 'integration tests' do
|
13
|
+
config <<-CONFIG
|
14
|
+
input {
|
15
|
+
generator {
|
16
|
+
message => '{"id": "abc", "app_name": "Web App", "event": "Hi - Dan"}'
|
17
|
+
codec => 'json'
|
18
|
+
count => 1
|
19
|
+
}
|
20
|
+
}
|
21
|
+
|
22
|
+
output {
|
23
|
+
charrington {
|
24
|
+
connection_string => 'jdbc:postgresql://localhost:5432/winston?user=postgres&password=postgres'
|
25
|
+
driver_jar_path => '/projects/logstash-output-charrington/vendor/postgresql-42.2.5.jar'
|
26
|
+
schema => 'dea'
|
27
|
+
}
|
28
|
+
}
|
29
|
+
CONFIG
|
30
|
+
|
31
|
+
agent do
|
32
|
+
puts "IT'S WORKING!!!!!"
|
33
|
+
end
|
34
|
+
end
|
35
|
+
|
36
|
+
# context 'running' do
|
37
|
+
# it 'should transform some JSON' do
|
38
|
+
# transformed = Charrington::Transform.call({"a" => 1})
|
39
|
+
# puts transformed
|
40
|
+
# end
|
41
|
+
# end
|
42
|
+
end
|
43
|
+
|
44
|
+
|
metadata
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: logstash-output-charrington
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.
|
4
|
+
version: 0.2.0
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- dconger
|
@@ -10,7 +10,7 @@ authors:
|
|
10
10
|
autorequire:
|
11
11
|
bindir: bin
|
12
12
|
cert_chain: []
|
13
|
-
date: 2019-
|
13
|
+
date: 2019-06-17 00:00:00.000000000 Z
|
14
14
|
dependencies:
|
15
15
|
- !ruby/object:Gem::Dependency
|
16
16
|
requirement: !ruby/object:Gem::Requirement
|
@@ -102,6 +102,34 @@ dependencies:
|
|
102
102
|
- - '='
|
103
103
|
- !ruby/object:Gem::Version
|
104
104
|
version: 0.41.2
|
105
|
+
- !ruby/object:Gem::Dependency
|
106
|
+
requirement: !ruby/object:Gem::Requirement
|
107
|
+
requirements:
|
108
|
+
- - ">="
|
109
|
+
- !ruby/object:Gem::Version
|
110
|
+
version: '0'
|
111
|
+
name: logstash-input-generator
|
112
|
+
prerelease: false
|
113
|
+
type: :development
|
114
|
+
version_requirements: !ruby/object:Gem::Requirement
|
115
|
+
requirements:
|
116
|
+
- - ">="
|
117
|
+
- !ruby/object:Gem::Version
|
118
|
+
version: '0'
|
119
|
+
- !ruby/object:Gem::Dependency
|
120
|
+
requirement: !ruby/object:Gem::Requirement
|
121
|
+
requirements:
|
122
|
+
- - ">="
|
123
|
+
- !ruby/object:Gem::Version
|
124
|
+
version: '0'
|
125
|
+
name: logstash-codec-json
|
126
|
+
prerelease: false
|
127
|
+
type: :development
|
128
|
+
version_requirements: !ruby/object:Gem::Requirement
|
129
|
+
requirements:
|
130
|
+
- - ">="
|
131
|
+
- !ruby/object:Gem::Version
|
132
|
+
version: '0'
|
105
133
|
description: This gem is a logstash plugin required to be installed on top of the
|
106
134
|
Logstash core pipeline using $LS_HOME/bin/logstash-plugin install 'logstash-output-charrington'.
|
107
135
|
This gem is not a stand-alone program
|
@@ -118,6 +146,12 @@ files:
|
|
118
146
|
- lib/com/zaxxer/HikariCP/2.7.2/HikariCP-2.7.2.jar
|
119
147
|
- lib/logstash-output-charrington_jars.rb
|
120
148
|
- lib/logstash/outputs/charrington.rb
|
149
|
+
- lib/logstash/outputs/charrington/alter_table.rb
|
150
|
+
- lib/logstash/outputs/charrington/create_table.rb
|
151
|
+
- lib/logstash/outputs/charrington/insert.rb
|
152
|
+
- lib/logstash/outputs/charrington/process.rb
|
153
|
+
- lib/logstash/outputs/charrington/service.rb
|
154
|
+
- lib/logstash/outputs/charrington/transform.rb
|
121
155
|
- lib/org/apache/logging/log4j/log4j-api/2.6.2/log4j-api-2.6.2.jar
|
122
156
|
- lib/org/apache/logging/log4j/log4j-slf4j-impl/2.6.2/log4j-slf4j-impl-2.6.2.jar
|
123
157
|
- lib/org/slf4j/slf4j-api/1.7.25/slf4j-api-1.7.25.jar
|