logstash-input-jdbc 4.3.11 → 4.3.12
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/CHANGELOG.md +3 -0
- data/NOTICE.TXT +1 -1
- data/lib/logstash/inputs/jdbc.rb +17 -7
- data/lib/logstash/plugin_mixins/jdbc/checked_count_logger.rb +38 -0
- data/lib/logstash/plugin_mixins/jdbc/jdbc.rb +317 -0
- data/lib/logstash/plugin_mixins/{value_tracking.rb → jdbc/value_tracking.rb} +2 -2
- data/logstash-input-jdbc.gemspec +1 -1
- data/spec/inputs/integ_spec.rb +36 -0
- data/spec/inputs/jdbc_spec.rb +50 -0
- metadata +7 -4
- data/lib/logstash/plugin_mixins/jdbc.rb +0 -313
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 68b7c5e096c835eea37700e2f7d68d14f61a6c67e5420e2ec3d1fd9a13cfb037
|
4
|
+
data.tar.gz: '0977958f3559d7a3e1e0f3c1b89c225e35f5b250fadcd2a5fba880423c6bd48c'
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: d94b92c9ba96ed2ad5ab908af35bcc5d552da3adb6d6ac821cc65d2392495eb7fa9e8d4e4cf0700a7ba43d27aa4743a0c8894f8baa3679cd4bc33a833fffbfb2
|
7
|
+
data.tar.gz: 89170a09b67a8629cd1fb7e9ff7d74d26ecd3055d412eb93855e0f39981b3e04c783333b6a5c6848cd985a0d858216fd5295d0e5ad0386f3aafd091d827ac8ab
|
data/CHANGELOG.md
CHANGED
@@ -1,3 +1,6 @@
|
|
1
|
+
## 4.3.12
|
2
|
+
- Added check to prevent count sql syntax errors when debug logging [Issue #287](https://github.com/logstash-plugins/logstash-input-jdbc/issue/287) and [Pull Request #294](https://github.com/logstash-plugins/logstash-input-jdbc/pull/294)
|
3
|
+
|
1
4
|
## 4.3.11
|
2
5
|
- Fixed crash that occurs when receiving string input that cannot be coerced to UTF-8 (such as BLOB data) [#291](https://github.com/logstash-plugins/logstash-input-jdbc/pull/291)
|
3
6
|
|
data/NOTICE.TXT
CHANGED
data/lib/logstash/inputs/jdbc.rb
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
# encoding: utf-8
|
2
2
|
require "logstash/inputs/base"
|
3
3
|
require "logstash/namespace"
|
4
|
-
require "logstash/plugin_mixins/jdbc"
|
4
|
+
require "logstash/plugin_mixins/jdbc/jdbc"
|
5
5
|
|
6
6
|
|
7
7
|
# This plugin was created as a way to ingest data from any database
|
@@ -123,8 +123,8 @@ require "logstash/plugin_mixins/jdbc"
|
|
123
123
|
# }
|
124
124
|
# ---------------------------------------------------------------------------------------------------
|
125
125
|
#
|
126
|
-
|
127
|
-
include LogStash::PluginMixins::Jdbc
|
126
|
+
module LogStash module Inputs class Jdbc < LogStash::Inputs::Base
|
127
|
+
include LogStash::PluginMixins::Jdbc::Jdbc
|
128
128
|
config_name "jdbc"
|
129
129
|
|
130
130
|
# If undefined, Logstash will complain, even if codec is unused.
|
@@ -213,7 +213,8 @@ class LogStash::Inputs::Jdbc < LogStash::Inputs::Base
|
|
213
213
|
end
|
214
214
|
end
|
215
215
|
|
216
|
-
|
216
|
+
set_value_tracker(LogStash::PluginMixins::Jdbc::ValueTracking.build_last_value_tracker(self))
|
217
|
+
set_statement_logger(LogStash::PluginMixins::Jdbc::CheckedCountLogger.new(@logger))
|
217
218
|
|
218
219
|
@enable_encoding = !@charset.nil? || !@columns_charset.empty?
|
219
220
|
|
@@ -221,13 +222,13 @@ class LogStash::Inputs::Jdbc < LogStash::Inputs::Base
|
|
221
222
|
raise(LogStash::ConfigurationError, "Must set either :statement or :statement_filepath. Only one may be set at a time.")
|
222
223
|
end
|
223
224
|
|
224
|
-
@statement = File.read(@statement_filepath) if @statement_filepath
|
225
|
+
@statement = ::File.read(@statement_filepath) if @statement_filepath
|
225
226
|
|
226
227
|
if (@jdbc_password_filepath and @jdbc_password)
|
227
228
|
raise(LogStash::ConfigurationError, "Only one of :jdbc_password, :jdbc_password_filepath may be set at a time.")
|
228
229
|
end
|
229
230
|
|
230
|
-
@jdbc_password = LogStash::Util::Password.new(File.read(@jdbc_password_filepath).strip) if @jdbc_password_filepath
|
231
|
+
@jdbc_password = LogStash::Util::Password.new(::File.read(@jdbc_password_filepath).strip) if @jdbc_password_filepath
|
231
232
|
|
232
233
|
if enable_encoding?
|
233
234
|
encodings = @columns_charset.values
|
@@ -241,6 +242,15 @@ class LogStash::Inputs::Jdbc < LogStash::Inputs::Base
|
|
241
242
|
end
|
242
243
|
end # def register
|
243
244
|
|
245
|
+
# test injection points
|
246
|
+
def set_statement_logger(instance)
|
247
|
+
@statement_logger = instance
|
248
|
+
end
|
249
|
+
|
250
|
+
def set_value_tracker(instance)
|
251
|
+
@value_tracker = instance
|
252
|
+
end
|
253
|
+
|
244
254
|
def run(queue)
|
245
255
|
if @schedule
|
246
256
|
@scheduler = Rufus::Scheduler.new(:max_work_threads => 1)
|
@@ -296,4 +306,4 @@ class LogStash::Inputs::Jdbc < LogStash::Inputs::Base
|
|
296
306
|
value
|
297
307
|
end
|
298
308
|
end
|
299
|
-
end # class LogStash::Inputs::Jdbc
|
309
|
+
end end end # class LogStash::Inputs::Jdbc
|
@@ -0,0 +1,38 @@
|
|
1
|
+
# encoding: utf-8
|
2
|
+
|
3
|
+
module LogStash module PluginMixins module Jdbc
|
4
|
+
class CheckedCountLogger
|
5
|
+
def initialize(logger)
|
6
|
+
@logger = logger
|
7
|
+
@needs_check = true
|
8
|
+
@count_is_supported = false
|
9
|
+
@in_debug = @logger.debug?
|
10
|
+
end
|
11
|
+
|
12
|
+
def log_statement_parameters(query, statement, parameters)
|
13
|
+
return unless @in_debug
|
14
|
+
check_count_query(query) if @needs_check
|
15
|
+
if @count_is_supported
|
16
|
+
@logger.debug("Executing JDBC query", :statement => statement, :parameters => parameters, :count => execute_count(query))
|
17
|
+
else
|
18
|
+
@logger.debug("Executing JDBC query", :statement => statement, :parameters => parameters)
|
19
|
+
end
|
20
|
+
end
|
21
|
+
|
22
|
+
def check_count_query(query)
|
23
|
+
@needs_check = false
|
24
|
+
begin
|
25
|
+
execute_count(query)
|
26
|
+
@count_is_supported = true
|
27
|
+
rescue Exception => e
|
28
|
+
@logger.warn("Attempting a count query raised an error, the generated count statement is most likely incorrect but check networking, authentication or your statement syntax", "exception" => e.message)
|
29
|
+
@logger.warn("Ongoing count statement generation is being prevented")
|
30
|
+
@count_is_supported = false
|
31
|
+
end
|
32
|
+
end
|
33
|
+
|
34
|
+
def execute_count(query)
|
35
|
+
query.count
|
36
|
+
end
|
37
|
+
end
|
38
|
+
end end end
|
@@ -0,0 +1,317 @@
|
|
1
|
+
# encoding: utf-8
|
2
|
+
# TAKEN FROM WIIBAA
|
3
|
+
require "logstash/config/mixin"
|
4
|
+
require "time"
|
5
|
+
require "date"
|
6
|
+
require_relative "value_tracking"
|
7
|
+
require_relative "checked_count_logger"
|
8
|
+
|
9
|
+
java_import java.util.concurrent.locks.ReentrantLock
|
10
|
+
|
11
|
+
# Tentative of abstracting JDBC logic to a mixin
|
12
|
+
# for potential reuse in other plugins (input/output)
|
13
|
+
module LogStash module PluginMixins module Jdbc
|
14
|
+
module Jdbc
|
15
|
+
# This method is called when someone includes this module
|
16
|
+
def self.included(base)
|
17
|
+
# Add these methods to the 'base' given.
|
18
|
+
base.extend(self)
|
19
|
+
base.setup_jdbc_config
|
20
|
+
end
|
21
|
+
|
22
|
+
|
23
|
+
public
|
24
|
+
def setup_jdbc_config
|
25
|
+
# JDBC driver library path to third party driver library. In case of multiple libraries being
|
26
|
+
# required you can pass them separated by a comma.
|
27
|
+
#
|
28
|
+
# If not provided, Plugin will look for the driver class in the Logstash Java classpath.
|
29
|
+
config :jdbc_driver_library, :validate => :string
|
30
|
+
|
31
|
+
# JDBC driver class to load, for exmaple, "org.apache.derby.jdbc.ClientDriver"
|
32
|
+
# NB per https://github.com/logstash-plugins/logstash-input-jdbc/issues/43 if you are using
|
33
|
+
# the Oracle JDBC driver (ojdbc6.jar) the correct `jdbc_driver_class` is `"Java::oracle.jdbc.driver.OracleDriver"`
|
34
|
+
config :jdbc_driver_class, :validate => :string, :required => true
|
35
|
+
|
36
|
+
# JDBC connection string
|
37
|
+
config :jdbc_connection_string, :validate => :string, :required => true
|
38
|
+
|
39
|
+
# JDBC user
|
40
|
+
config :jdbc_user, :validate => :string, :required => true
|
41
|
+
|
42
|
+
# JDBC password
|
43
|
+
config :jdbc_password, :validate => :password
|
44
|
+
|
45
|
+
# JDBC password filename
|
46
|
+
config :jdbc_password_filepath, :validate => :path
|
47
|
+
|
48
|
+
# JDBC enable paging
|
49
|
+
#
|
50
|
+
# This will cause a sql statement to be broken up into multiple queries.
|
51
|
+
# Each query will use limits and offsets to collectively retrieve the full
|
52
|
+
# result-set. The limit size is set with `jdbc_page_size`.
|
53
|
+
#
|
54
|
+
# Be aware that ordering is not guaranteed between queries.
|
55
|
+
config :jdbc_paging_enabled, :validate => :boolean, :default => false
|
56
|
+
|
57
|
+
# JDBC page size
|
58
|
+
config :jdbc_page_size, :validate => :number, :default => 100000
|
59
|
+
|
60
|
+
# JDBC fetch size. if not provided, respective driver's default will be used
|
61
|
+
config :jdbc_fetch_size, :validate => :number
|
62
|
+
|
63
|
+
# Connection pool configuration.
|
64
|
+
# Validate connection before use.
|
65
|
+
config :jdbc_validate_connection, :validate => :boolean, :default => false
|
66
|
+
|
67
|
+
# Connection pool configuration.
|
68
|
+
# How often to validate a connection (in seconds)
|
69
|
+
config :jdbc_validation_timeout, :validate => :number, :default => 3600
|
70
|
+
|
71
|
+
# Connection pool configuration.
|
72
|
+
# The amount of seconds to wait to acquire a connection before raising a PoolTimeoutError (default 5)
|
73
|
+
config :jdbc_pool_timeout, :validate => :number, :default => 5
|
74
|
+
|
75
|
+
# Timezone conversion.
|
76
|
+
# SQL does not allow for timezone data in timestamp fields. This plugin will automatically
|
77
|
+
# convert your SQL timestamp fields to Logstash timestamps, in relative UTC time in ISO8601 format.
|
78
|
+
#
|
79
|
+
# Using this setting will manually assign a specified timezone offset, instead
|
80
|
+
# of using the timezone setting of the local machine. You must use a canonical
|
81
|
+
# timezone, *America/Denver*, for example.
|
82
|
+
config :jdbc_default_timezone, :validate => :string
|
83
|
+
|
84
|
+
# General/Vendor-specific Sequel configuration options.
|
85
|
+
#
|
86
|
+
# An example of an optional connection pool configuration
|
87
|
+
# max_connections - The maximum number of connections the connection pool
|
88
|
+
#
|
89
|
+
# examples of vendor-specific options can be found in this
|
90
|
+
# documentation page: https://github.com/jeremyevans/sequel/blob/master/doc/opening_databases.rdoc
|
91
|
+
config :sequel_opts, :validate => :hash, :default => {}
|
92
|
+
|
93
|
+
# Log level at which to log SQL queries, the accepted values are the common ones fatal, error, warn,
|
94
|
+
# info and debug. The default value is info.
|
95
|
+
config :sql_log_level, :validate => [ "fatal", "error", "warn", "info", "debug" ], :default => "info"
|
96
|
+
|
97
|
+
# Maximum number of times to try connecting to database
|
98
|
+
config :connection_retry_attempts, :validate => :number, :default => 1
|
99
|
+
# Number of seconds to sleep between connection attempts
|
100
|
+
config :connection_retry_attempts_wait_time, :validate => :number, :default => 0.5
|
101
|
+
end
|
102
|
+
|
103
|
+
private
|
104
|
+
def jdbc_connect
|
105
|
+
opts = {
|
106
|
+
:user => @jdbc_user,
|
107
|
+
:password => @jdbc_password.nil? ? nil : @jdbc_password.value,
|
108
|
+
:pool_timeout => @jdbc_pool_timeout,
|
109
|
+
:keep_reference => false
|
110
|
+
}.merge(@sequel_opts)
|
111
|
+
retry_attempts = @connection_retry_attempts
|
112
|
+
loop do
|
113
|
+
retry_attempts -= 1
|
114
|
+
begin
|
115
|
+
return Sequel.connect(@jdbc_connection_string, opts=opts)
|
116
|
+
rescue Sequel::PoolTimeout => e
|
117
|
+
if retry_attempts <= 0
|
118
|
+
@logger.error("Failed to connect to database. #{@jdbc_pool_timeout} second timeout exceeded. Tried #{@connection_retry_attempts} times.")
|
119
|
+
raise e
|
120
|
+
else
|
121
|
+
@logger.error("Failed to connect to database. #{@jdbc_pool_timeout} second timeout exceeded. Trying again.")
|
122
|
+
end
|
123
|
+
rescue Sequel::Error => e
|
124
|
+
if retry_attempts <= 0
|
125
|
+
@logger.error("Unable to connect to database. Tried #{@connection_retry_attempts} times", :error_message => e.message, )
|
126
|
+
raise e
|
127
|
+
else
|
128
|
+
@logger.error("Unable to connect to database. Trying again", :error_message => e.message)
|
129
|
+
end
|
130
|
+
end
|
131
|
+
sleep(@connection_retry_attempts_wait_time)
|
132
|
+
end
|
133
|
+
end
|
134
|
+
|
135
|
+
private
|
136
|
+
def load_drivers(drivers)
|
137
|
+
drivers.each do |driver|
|
138
|
+
begin
|
139
|
+
class_loader = java.lang.ClassLoader.getSystemClassLoader().to_java(java.net.URLClassLoader)
|
140
|
+
class_loader.add_url(java.io.File.new(driver).toURI().toURL())
|
141
|
+
rescue => e
|
142
|
+
@logger.error("Failed to load #{driver}", :exception => e)
|
143
|
+
end
|
144
|
+
end
|
145
|
+
end
|
146
|
+
|
147
|
+
private
|
148
|
+
def open_jdbc_connection
|
149
|
+
require "java"
|
150
|
+
require "sequel"
|
151
|
+
require "sequel/adapters/jdbc"
|
152
|
+
load_drivers(@jdbc_driver_library.split(",")) if @jdbc_driver_library
|
153
|
+
|
154
|
+
begin
|
155
|
+
Sequel::JDBC.load_driver(@jdbc_driver_class)
|
156
|
+
rescue Sequel::AdapterNotFound => e
|
157
|
+
message = if @jdbc_driver_library.nil?
|
158
|
+
":jdbc_driver_library is not set, are you sure you included
|
159
|
+
the proper driver client libraries in your classpath?"
|
160
|
+
else
|
161
|
+
"Are you sure you've included the correct jdbc driver in :jdbc_driver_library?"
|
162
|
+
end
|
163
|
+
raise LogStash::ConfigurationError, "#{e}. #{message}"
|
164
|
+
end
|
165
|
+
@database = jdbc_connect()
|
166
|
+
@database.extension(:pagination)
|
167
|
+
if @jdbc_default_timezone
|
168
|
+
@database.extension(:named_timezones)
|
169
|
+
@database.timezone = @jdbc_default_timezone
|
170
|
+
end
|
171
|
+
if @jdbc_validate_connection
|
172
|
+
@database.extension(:connection_validator)
|
173
|
+
@database.pool.connection_validation_timeout = @jdbc_validation_timeout
|
174
|
+
end
|
175
|
+
@database.fetch_size = @jdbc_fetch_size unless @jdbc_fetch_size.nil?
|
176
|
+
begin
|
177
|
+
@database.test_connection
|
178
|
+
rescue Sequel::DatabaseConnectionError => e
|
179
|
+
@logger.warn("Failed test_connection.", :exception => e)
|
180
|
+
close_jdbc_connection
|
181
|
+
|
182
|
+
#TODO return false and let the plugin raise a LogStash::ConfigurationError
|
183
|
+
raise e
|
184
|
+
end
|
185
|
+
|
186
|
+
@database.sql_log_level = @sql_log_level.to_sym
|
187
|
+
@database.logger = @logger
|
188
|
+
|
189
|
+
@database.extension :identifier_mangling
|
190
|
+
|
191
|
+
if @lowercase_column_names
|
192
|
+
@database.identifier_output_method = :downcase
|
193
|
+
else
|
194
|
+
@database.identifier_output_method = :to_s
|
195
|
+
end
|
196
|
+
end
|
197
|
+
|
198
|
+
public
|
199
|
+
def prepare_jdbc_connection
|
200
|
+
@connection_lock = ReentrantLock.new
|
201
|
+
end
|
202
|
+
|
203
|
+
public
|
204
|
+
def close_jdbc_connection
|
205
|
+
begin
|
206
|
+
# pipeline restarts can also close the jdbc connection, block until the current executing statement is finished to avoid leaking connections
|
207
|
+
# connections in use won't really get closed
|
208
|
+
@connection_lock.lock
|
209
|
+
@database.disconnect if @database
|
210
|
+
rescue => e
|
211
|
+
@logger.warn("Failed to close connection", :exception => e)
|
212
|
+
ensure
|
213
|
+
@connection_lock.unlock
|
214
|
+
end
|
215
|
+
end
|
216
|
+
|
217
|
+
public
|
218
|
+
def execute_statement(statement, parameters)
|
219
|
+
success = false
|
220
|
+
@connection_lock.lock
|
221
|
+
open_jdbc_connection
|
222
|
+
begin
|
223
|
+
params = symbolized_params(parameters)
|
224
|
+
query = @database[statement, params]
|
225
|
+
|
226
|
+
sql_last_value = @use_column_value ? @value_tracker.value : Time.now.utc
|
227
|
+
@tracking_column_warning_sent = false
|
228
|
+
@statement_logger.log_statement_parameters(query, statement, params)
|
229
|
+
perform_query(query) do |row|
|
230
|
+
sql_last_value = get_column_value(row) if @use_column_value
|
231
|
+
yield extract_values_from(row)
|
232
|
+
end
|
233
|
+
success = true
|
234
|
+
rescue Sequel::DatabaseConnectionError, Sequel::DatabaseError => e
|
235
|
+
@logger.warn("Exception when executing JDBC query", :exception => e)
|
236
|
+
else
|
237
|
+
@value_tracker.set_value(sql_last_value)
|
238
|
+
ensure
|
239
|
+
close_jdbc_connection
|
240
|
+
@connection_lock.unlock
|
241
|
+
end
|
242
|
+
return success
|
243
|
+
end
|
244
|
+
|
245
|
+
# Performs the query, respecting our pagination settings, yielding once per row of data
|
246
|
+
# @param query [Sequel::Dataset]
|
247
|
+
# @yieldparam row [Hash{Symbol=>Object}]
|
248
|
+
private
|
249
|
+
def perform_query(query)
|
250
|
+
if @jdbc_paging_enabled
|
251
|
+
query.each_page(@jdbc_page_size) do |paged_dataset|
|
252
|
+
paged_dataset.each do |row|
|
253
|
+
yield row
|
254
|
+
end
|
255
|
+
end
|
256
|
+
else
|
257
|
+
query.each do |row|
|
258
|
+
yield row
|
259
|
+
end
|
260
|
+
end
|
261
|
+
end
|
262
|
+
|
263
|
+
public
|
264
|
+
def get_column_value(row)
|
265
|
+
if !row.has_key?(@tracking_column.to_sym)
|
266
|
+
if !@tracking_column_warning_sent
|
267
|
+
@logger.warn("tracking_column not found in dataset.", :tracking_column => @tracking_column)
|
268
|
+
@tracking_column_warning_sent = true
|
269
|
+
end
|
270
|
+
# If we can't find the tracking column, return the current value in the ivar
|
271
|
+
@sql_last_value
|
272
|
+
else
|
273
|
+
# Otherwise send the updated tracking column
|
274
|
+
row[@tracking_column.to_sym]
|
275
|
+
end
|
276
|
+
end
|
277
|
+
|
278
|
+
# Symbolize parameters keys to use with Sequel
|
279
|
+
private
|
280
|
+
def symbolized_params(parameters)
|
281
|
+
parameters.inject({}) do |hash,(k,v)|
|
282
|
+
case v
|
283
|
+
when LogStash::Timestamp
|
284
|
+
hash[k.to_sym] = v.time
|
285
|
+
else
|
286
|
+
hash[k.to_sym] = v
|
287
|
+
end
|
288
|
+
hash
|
289
|
+
end
|
290
|
+
end
|
291
|
+
|
292
|
+
private
|
293
|
+
#Stringify row keys and decorate values when necessary
|
294
|
+
def extract_values_from(row)
|
295
|
+
Hash[row.map { |k, v| [k.to_s, decorate_value(v)] }]
|
296
|
+
end
|
297
|
+
|
298
|
+
private
|
299
|
+
def decorate_value(value)
|
300
|
+
if value.is_a?(Time)
|
301
|
+
# transform it to LogStash::Timestamp as required by LS
|
302
|
+
LogStash::Timestamp.new(value)
|
303
|
+
elsif value.is_a?(Date)
|
304
|
+
LogStash::Timestamp.new(value.to_time)
|
305
|
+
elsif value.is_a?(DateTime)
|
306
|
+
# Manual timezone conversion detected.
|
307
|
+
# This is slower, so we put it in as a conditional case.
|
308
|
+
LogStash::Timestamp.new(Time.parse(value.to_s))
|
309
|
+
else
|
310
|
+
value
|
311
|
+
end
|
312
|
+
end
|
313
|
+
|
314
|
+
end
|
315
|
+
end end end
|
316
|
+
|
317
|
+
|
@@ -1,7 +1,7 @@
|
|
1
1
|
# encoding: utf-8
|
2
2
|
require "yaml" # persistence
|
3
3
|
|
4
|
-
module LogStash module PluginMixins
|
4
|
+
module LogStash module PluginMixins module Jdbc
|
5
5
|
class ValueTracking
|
6
6
|
|
7
7
|
def self.build_last_value_tracker(plugin)
|
@@ -125,4 +125,4 @@ module LogStash module PluginMixins
|
|
125
125
|
def write(value)
|
126
126
|
end
|
127
127
|
end
|
128
|
-
end end
|
128
|
+
end end end
|
data/logstash-input-jdbc.gemspec
CHANGED
@@ -1,6 +1,6 @@
|
|
1
1
|
Gem::Specification.new do |s|
|
2
2
|
s.name = 'logstash-input-jdbc'
|
3
|
-
s.version = '4.3.
|
3
|
+
s.version = '4.3.12'
|
4
4
|
s.licenses = ['Apache License (2.0)']
|
5
5
|
s.summary = "Creates events from JDBC data"
|
6
6
|
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 gemname. This gem is not a stand-alone program"
|
@@ -0,0 +1,36 @@
|
|
1
|
+
require "logstash/devutils/rspec/spec_helper"
|
2
|
+
require "logstash/inputs/jdbc"
|
3
|
+
|
4
|
+
# This test requires: Firebird installed to Mac OSX, it uses the built-in example database `employee`
|
5
|
+
|
6
|
+
describe LogStash::Inputs::Jdbc, :integration => true do
|
7
|
+
# This is a necessary change test-wide to guarantee that no local timezone
|
8
|
+
# is picked up. It could be arbitrarily set to any timezone, but then the test
|
9
|
+
# would have to compensate differently. That's why UTC is chosen.
|
10
|
+
ENV["TZ"] = "Etc/UTC"
|
11
|
+
let(:mixin_settings) do
|
12
|
+
{ "jdbc_user" => "SYSDBA", "jdbc_driver_class" => "org.firebirdsql.jdbc.FBDriver", "jdbc_driver_library" => "/elastic/tmp/jaybird-full-3.0.4.jar",
|
13
|
+
"jdbc_connection_string" => "jdbc:firebirdsql://localhost:3050//Library/Frameworks/Firebird.framework/Versions/A/Resources/examples/empbuild/employee.fdb", "jdbc_password" => "masterkey"}
|
14
|
+
end
|
15
|
+
let(:settings) { {"statement" => "SELECT FIRST_NAME, LAST_NAME FROM EMPLOYEE WHERE EMP_NO > 144"} }
|
16
|
+
let(:plugin) { LogStash::Inputs::Jdbc.new(mixin_settings.merge(settings)) }
|
17
|
+
let(:queue) { Queue.new }
|
18
|
+
|
19
|
+
context "when passing no parameters" do
|
20
|
+
before do
|
21
|
+
plugin.register
|
22
|
+
end
|
23
|
+
|
24
|
+
after do
|
25
|
+
plugin.stop
|
26
|
+
end
|
27
|
+
|
28
|
+
it "should retrieve params correctly from Event" do
|
29
|
+
plugin.run(queue)
|
30
|
+
event = queue.pop
|
31
|
+
expect(event.get('first_name')).to eq("Mark")
|
32
|
+
expect(event.get('last_name')).to eq("Guckenheimer")
|
33
|
+
end
|
34
|
+
end
|
35
|
+
end
|
36
|
+
|
data/spec/inputs/jdbc_spec.rb
CHANGED
@@ -1267,4 +1267,54 @@ describe LogStash::Inputs::Jdbc do
|
|
1267
1267
|
expect(event.get("ranking").to_f).to eq(95.67)
|
1268
1268
|
end
|
1269
1269
|
end
|
1270
|
+
|
1271
|
+
context "when debug logging and a count query raises a count related error" do
|
1272
|
+
let(:settings) do
|
1273
|
+
{ "statement" => "SELECT * from types_table" }
|
1274
|
+
end
|
1275
|
+
let(:logger) { double("logger", :debug? => true) }
|
1276
|
+
let(:statement_logger) { LogStash::PluginMixins::Jdbc::CheckedCountLogger.new(logger) }
|
1277
|
+
let(:value_tracker) { double("value tracker", :set_value => nil, :write => nil) }
|
1278
|
+
let(:msg) { 'Java::JavaSql::SQLSyntaxErrorException: Dynamic SQL Error; SQL error code = -104; Token unknown - line 1, column 105; LIMIT [SQLState:42000, ISC error code:335544634]' }
|
1279
|
+
let(:error_args) do
|
1280
|
+
{"exception" => msg}
|
1281
|
+
end
|
1282
|
+
|
1283
|
+
before do
|
1284
|
+
db << "INSERT INTO types_table (num, string, started_at, custom_time, ranking) VALUES (1, 'A test', '1999-12-31', '1999-12-31 23:59:59', 95.67)"
|
1285
|
+
plugin.register
|
1286
|
+
plugin.set_statement_logger(statement_logger)
|
1287
|
+
plugin.set_value_tracker(value_tracker)
|
1288
|
+
allow(value_tracker).to receive(:value).and_return("bar")
|
1289
|
+
allow(statement_logger).to receive(:execute_count).once.and_raise(StandardError.new(msg))
|
1290
|
+
end
|
1291
|
+
|
1292
|
+
after do
|
1293
|
+
plugin.stop
|
1294
|
+
end
|
1295
|
+
|
1296
|
+
context "if the count query raises an error" do
|
1297
|
+
it "should log a debug line without a count key as its unknown whether a count works at this stage" do
|
1298
|
+
expect(logger).to receive(:warn).once.with("Attempting a count query raised an error, the generated count statement is most likely incorrect but check networking, authentication or your statement syntax", error_args)
|
1299
|
+
expect(logger).to receive(:warn).once.with("Ongoing count statement generation is being prevented")
|
1300
|
+
expect(logger).to receive(:debug).once.with("Executing JDBC query", :statement => settings["statement"], :parameters => {:sql_last_value=>"bar"})
|
1301
|
+
plugin.run(queue)
|
1302
|
+
queue.pop
|
1303
|
+
end
|
1304
|
+
|
1305
|
+
it "should create an event normally" do
|
1306
|
+
allow(logger).to receive(:warn)
|
1307
|
+
allow(logger).to receive(:debug)
|
1308
|
+
plugin.run(queue)
|
1309
|
+
event = queue.pop
|
1310
|
+
expect(event.get("num")).to eq(1)
|
1311
|
+
expect(event.get("string")).to eq("A test")
|
1312
|
+
expect(event.get("started_at")).to be_a(LogStash::Timestamp)
|
1313
|
+
expect(event.get("started_at").to_s).to eq("1999-12-31T00:00:00.000Z")
|
1314
|
+
expect(event.get("custom_time")).to be_a(LogStash::Timestamp)
|
1315
|
+
expect(event.get("custom_time").to_s).to eq("1999-12-31T23:59:59.000Z")
|
1316
|
+
expect(event.get("ranking").to_f).to eq(95.67)
|
1317
|
+
end
|
1318
|
+
end
|
1319
|
+
end
|
1270
1320
|
end
|
metadata
CHANGED
@@ -1,14 +1,14 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: logstash-input-jdbc
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 4.3.
|
4
|
+
version: 4.3.12
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Elastic
|
8
8
|
autorequire:
|
9
9
|
bindir: bin
|
10
10
|
cert_chain: []
|
11
|
-
date: 2018-
|
11
|
+
date: 2018-08-29 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
requirement: !ruby/object:Gem::Requirement
|
@@ -171,9 +171,11 @@ files:
|
|
171
171
|
- README.md
|
172
172
|
- docs/index.asciidoc
|
173
173
|
- lib/logstash/inputs/jdbc.rb
|
174
|
-
- lib/logstash/plugin_mixins/jdbc.rb
|
175
|
-
- lib/logstash/plugin_mixins/
|
174
|
+
- lib/logstash/plugin_mixins/jdbc/checked_count_logger.rb
|
175
|
+
- lib/logstash/plugin_mixins/jdbc/jdbc.rb
|
176
|
+
- lib/logstash/plugin_mixins/jdbc/value_tracking.rb
|
176
177
|
- logstash-input-jdbc.gemspec
|
178
|
+
- spec/inputs/integ_spec.rb
|
177
179
|
- spec/inputs/jdbc_spec.rb
|
178
180
|
homepage: http://www.elastic.co/guide/en/logstash/current/index.html
|
179
181
|
licenses:
|
@@ -202,4 +204,5 @@ signing_key:
|
|
202
204
|
specification_version: 4
|
203
205
|
summary: Creates events from JDBC data
|
204
206
|
test_files:
|
207
|
+
- spec/inputs/integ_spec.rb
|
205
208
|
- spec/inputs/jdbc_spec.rb
|
@@ -1,313 +0,0 @@
|
|
1
|
-
# encoding: utf-8
|
2
|
-
# TAKEN FROM WIIBAA
|
3
|
-
require "logstash/config/mixin"
|
4
|
-
require "time"
|
5
|
-
require "date"
|
6
|
-
require "logstash/plugin_mixins/value_tracking"
|
7
|
-
|
8
|
-
java_import java.util.concurrent.locks.ReentrantLock
|
9
|
-
|
10
|
-
# Tentative of abstracting JDBC logic to a mixin
|
11
|
-
# for potential reuse in other plugins (input/output)
|
12
|
-
module LogStash::PluginMixins::Jdbc
|
13
|
-
|
14
|
-
# This method is called when someone includes this module
|
15
|
-
def self.included(base)
|
16
|
-
# Add these methods to the 'base' given.
|
17
|
-
base.extend(self)
|
18
|
-
base.setup_jdbc_config
|
19
|
-
end
|
20
|
-
|
21
|
-
|
22
|
-
public
|
23
|
-
def setup_jdbc_config
|
24
|
-
# JDBC driver library path to third party driver library. In case of multiple libraries being
|
25
|
-
# required you can pass them separated by a comma.
|
26
|
-
#
|
27
|
-
# If not provided, Plugin will look for the driver class in the Logstash Java classpath.
|
28
|
-
config :jdbc_driver_library, :validate => :string
|
29
|
-
|
30
|
-
# JDBC driver class to load, for exmaple, "org.apache.derby.jdbc.ClientDriver"
|
31
|
-
# NB per https://github.com/logstash-plugins/logstash-input-jdbc/issues/43 if you are using
|
32
|
-
# the Oracle JDBC driver (ojdbc6.jar) the correct `jdbc_driver_class` is `"Java::oracle.jdbc.driver.OracleDriver"`
|
33
|
-
config :jdbc_driver_class, :validate => :string, :required => true
|
34
|
-
|
35
|
-
# JDBC connection string
|
36
|
-
config :jdbc_connection_string, :validate => :string, :required => true
|
37
|
-
|
38
|
-
# JDBC user
|
39
|
-
config :jdbc_user, :validate => :string, :required => true
|
40
|
-
|
41
|
-
# JDBC password
|
42
|
-
config :jdbc_password, :validate => :password
|
43
|
-
|
44
|
-
# JDBC password filename
|
45
|
-
config :jdbc_password_filepath, :validate => :path
|
46
|
-
|
47
|
-
# JDBC enable paging
|
48
|
-
#
|
49
|
-
# This will cause a sql statement to be broken up into multiple queries.
|
50
|
-
# Each query will use limits and offsets to collectively retrieve the full
|
51
|
-
# result-set. The limit size is set with `jdbc_page_size`.
|
52
|
-
#
|
53
|
-
# Be aware that ordering is not guaranteed between queries.
|
54
|
-
config :jdbc_paging_enabled, :validate => :boolean, :default => false
|
55
|
-
|
56
|
-
# JDBC page size
|
57
|
-
config :jdbc_page_size, :validate => :number, :default => 100000
|
58
|
-
|
59
|
-
# JDBC fetch size. if not provided, respective driver's default will be used
|
60
|
-
config :jdbc_fetch_size, :validate => :number
|
61
|
-
|
62
|
-
# Connection pool configuration.
|
63
|
-
# Validate connection before use.
|
64
|
-
config :jdbc_validate_connection, :validate => :boolean, :default => false
|
65
|
-
|
66
|
-
# Connection pool configuration.
|
67
|
-
# How often to validate a connection (in seconds)
|
68
|
-
config :jdbc_validation_timeout, :validate => :number, :default => 3600
|
69
|
-
|
70
|
-
# Connection pool configuration.
|
71
|
-
# The amount of seconds to wait to acquire a connection before raising a PoolTimeoutError (default 5)
|
72
|
-
config :jdbc_pool_timeout, :validate => :number, :default => 5
|
73
|
-
|
74
|
-
# Timezone conversion.
|
75
|
-
# SQL does not allow for timezone data in timestamp fields. This plugin will automatically
|
76
|
-
# convert your SQL timestamp fields to Logstash timestamps, in relative UTC time in ISO8601 format.
|
77
|
-
#
|
78
|
-
# Using this setting will manually assign a specified timezone offset, instead
|
79
|
-
# of using the timezone setting of the local machine. You must use a canonical
|
80
|
-
# timezone, *America/Denver*, for example.
|
81
|
-
config :jdbc_default_timezone, :validate => :string
|
82
|
-
|
83
|
-
# General/Vendor-specific Sequel configuration options.
|
84
|
-
#
|
85
|
-
# An example of an optional connection pool configuration
|
86
|
-
# max_connections - The maximum number of connections the connection pool
|
87
|
-
#
|
88
|
-
# examples of vendor-specific options can be found in this
|
89
|
-
# documentation page: https://github.com/jeremyevans/sequel/blob/master/doc/opening_databases.rdoc
|
90
|
-
config :sequel_opts, :validate => :hash, :default => {}
|
91
|
-
|
92
|
-
# Log level at which to log SQL queries, the accepted values are the common ones fatal, error, warn,
|
93
|
-
# info and debug. The default value is info.
|
94
|
-
config :sql_log_level, :validate => [ "fatal", "error", "warn", "info", "debug" ], :default => "info"
|
95
|
-
|
96
|
-
# Maximum number of times to try connecting to database
|
97
|
-
config :connection_retry_attempts, :validate => :number, :default => 1
|
98
|
-
# Number of seconds to sleep between connection attempts
|
99
|
-
config :connection_retry_attempts_wait_time, :validate => :number, :default => 0.5
|
100
|
-
end
|
101
|
-
|
102
|
-
private
|
103
|
-
def jdbc_connect
|
104
|
-
opts = {
|
105
|
-
:user => @jdbc_user,
|
106
|
-
:password => @jdbc_password.nil? ? nil : @jdbc_password.value,
|
107
|
-
:pool_timeout => @jdbc_pool_timeout,
|
108
|
-
:keep_reference => false
|
109
|
-
}.merge(@sequel_opts)
|
110
|
-
retry_attempts = @connection_retry_attempts
|
111
|
-
loop do
|
112
|
-
retry_attempts -= 1
|
113
|
-
begin
|
114
|
-
return Sequel.connect(@jdbc_connection_string, opts=opts)
|
115
|
-
rescue Sequel::PoolTimeout => e
|
116
|
-
if retry_attempts <= 0
|
117
|
-
@logger.error("Failed to connect to database. #{@jdbc_pool_timeout} second timeout exceeded. Tried #{@connection_retry_attempts} times.")
|
118
|
-
raise e
|
119
|
-
else
|
120
|
-
@logger.error("Failed to connect to database. #{@jdbc_pool_timeout} second timeout exceeded. Trying again.")
|
121
|
-
end
|
122
|
-
rescue Sequel::Error => e
|
123
|
-
if retry_attempts <= 0
|
124
|
-
@logger.error("Unable to connect to database. Tried #{@connection_retry_attempts} times", :error_message => e.message, )
|
125
|
-
raise e
|
126
|
-
else
|
127
|
-
@logger.error("Unable to connect to database. Trying again", :error_message => e.message)
|
128
|
-
end
|
129
|
-
end
|
130
|
-
sleep(@connection_retry_attempts_wait_time)
|
131
|
-
end
|
132
|
-
end
|
133
|
-
|
134
|
-
private
|
135
|
-
def load_drivers(drivers)
|
136
|
-
drivers.each do |driver|
|
137
|
-
begin
|
138
|
-
class_loader = java.lang.ClassLoader.getSystemClassLoader().to_java(java.net.URLClassLoader)
|
139
|
-
class_loader.add_url(java.io.File.new(driver).toURI().toURL())
|
140
|
-
rescue => e
|
141
|
-
@logger.error("Failed to load #{driver}", :exception => e)
|
142
|
-
end
|
143
|
-
end
|
144
|
-
end
|
145
|
-
|
146
|
-
private
|
147
|
-
def open_jdbc_connection
|
148
|
-
require "java"
|
149
|
-
require "sequel"
|
150
|
-
require "sequel/adapters/jdbc"
|
151
|
-
load_drivers(@jdbc_driver_library.split(",")) if @jdbc_driver_library
|
152
|
-
|
153
|
-
begin
|
154
|
-
Sequel::JDBC.load_driver(@jdbc_driver_class)
|
155
|
-
rescue Sequel::AdapterNotFound => e
|
156
|
-
message = if @jdbc_driver_library.nil?
|
157
|
-
":jdbc_driver_library is not set, are you sure you included
|
158
|
-
the proper driver client libraries in your classpath?"
|
159
|
-
else
|
160
|
-
"Are you sure you've included the correct jdbc driver in :jdbc_driver_library?"
|
161
|
-
end
|
162
|
-
raise LogStash::ConfigurationError, "#{e}. #{message}"
|
163
|
-
end
|
164
|
-
@database = jdbc_connect()
|
165
|
-
@database.extension(:pagination)
|
166
|
-
if @jdbc_default_timezone
|
167
|
-
@database.extension(:named_timezones)
|
168
|
-
@database.timezone = @jdbc_default_timezone
|
169
|
-
end
|
170
|
-
if @jdbc_validate_connection
|
171
|
-
@database.extension(:connection_validator)
|
172
|
-
@database.pool.connection_validation_timeout = @jdbc_validation_timeout
|
173
|
-
end
|
174
|
-
@database.fetch_size = @jdbc_fetch_size unless @jdbc_fetch_size.nil?
|
175
|
-
begin
|
176
|
-
@database.test_connection
|
177
|
-
rescue Sequel::DatabaseConnectionError => e
|
178
|
-
@logger.warn("Failed test_connection.", :exception => e)
|
179
|
-
close_jdbc_connection
|
180
|
-
|
181
|
-
#TODO return false and let the plugin raise a LogStash::ConfigurationError
|
182
|
-
raise e
|
183
|
-
end
|
184
|
-
|
185
|
-
@database.sql_log_level = @sql_log_level.to_sym
|
186
|
-
@database.logger = @logger
|
187
|
-
|
188
|
-
@database.extension :identifier_mangling
|
189
|
-
|
190
|
-
if @lowercase_column_names
|
191
|
-
@database.identifier_output_method = :downcase
|
192
|
-
else
|
193
|
-
@database.identifier_output_method = :to_s
|
194
|
-
end
|
195
|
-
end
|
196
|
-
|
197
|
-
public
|
198
|
-
def prepare_jdbc_connection
|
199
|
-
@connection_lock = ReentrantLock.new
|
200
|
-
end
|
201
|
-
|
202
|
-
public
|
203
|
-
def close_jdbc_connection
|
204
|
-
begin
|
205
|
-
# pipeline restarts can also close the jdbc connection, block until the current executing statement is finished to avoid leaking connections
|
206
|
-
# connections in use won't really get closed
|
207
|
-
@connection_lock.lock
|
208
|
-
@database.disconnect if @database
|
209
|
-
rescue => e
|
210
|
-
@logger.warn("Failed to close connection", :exception => e)
|
211
|
-
ensure
|
212
|
-
@connection_lock.unlock
|
213
|
-
end
|
214
|
-
end
|
215
|
-
|
216
|
-
public
|
217
|
-
def execute_statement(statement, parameters)
|
218
|
-
success = false
|
219
|
-
@connection_lock.lock
|
220
|
-
open_jdbc_connection
|
221
|
-
begin
|
222
|
-
parameters = symbolized_params(parameters)
|
223
|
-
query = @database[statement, parameters]
|
224
|
-
|
225
|
-
sql_last_value = @use_column_value ? @value_tracker.value : Time.now.utc
|
226
|
-
@tracking_column_warning_sent = false
|
227
|
-
@logger.debug? and @logger.debug("Executing JDBC query", :statement => statement, :parameters => parameters, :count => query.count)
|
228
|
-
|
229
|
-
perform_query(query) do |row|
|
230
|
-
sql_last_value = get_column_value(row) if @use_column_value
|
231
|
-
yield extract_values_from(row)
|
232
|
-
end
|
233
|
-
success = true
|
234
|
-
rescue Sequel::DatabaseConnectionError, Sequel::DatabaseError => e
|
235
|
-
@logger.warn("Exception when executing JDBC query", :exception => e)
|
236
|
-
else
|
237
|
-
@value_tracker.set_value(sql_last_value)
|
238
|
-
ensure
|
239
|
-
close_jdbc_connection
|
240
|
-
@connection_lock.unlock
|
241
|
-
end
|
242
|
-
return success
|
243
|
-
end
|
244
|
-
|
245
|
-
# Performs the query, respecting our pagination settings, yielding once per row of data
|
246
|
-
# @param query [Sequel::Dataset]
|
247
|
-
# @yieldparam row [Hash{Symbol=>Object}]
|
248
|
-
private
|
249
|
-
def perform_query(query)
|
250
|
-
if @jdbc_paging_enabled
|
251
|
-
query.each_page(@jdbc_page_size) do |paged_dataset|
|
252
|
-
paged_dataset.each do |row|
|
253
|
-
yield row
|
254
|
-
end
|
255
|
-
end
|
256
|
-
else
|
257
|
-
query.each do |row|
|
258
|
-
yield row
|
259
|
-
end
|
260
|
-
end
|
261
|
-
end
|
262
|
-
|
263
|
-
public
|
264
|
-
def get_column_value(row)
|
265
|
-
if !row.has_key?(@tracking_column.to_sym)
|
266
|
-
if !@tracking_column_warning_sent
|
267
|
-
@logger.warn("tracking_column not found in dataset.", :tracking_column => @tracking_column)
|
268
|
-
@tracking_column_warning_sent = true
|
269
|
-
end
|
270
|
-
# If we can't find the tracking column, return the current value in the ivar
|
271
|
-
@sql_last_value
|
272
|
-
else
|
273
|
-
# Otherwise send the updated tracking column
|
274
|
-
row[@tracking_column.to_sym]
|
275
|
-
end
|
276
|
-
end
|
277
|
-
|
278
|
-
# Symbolize parameters keys to use with Sequel
|
279
|
-
private
|
280
|
-
def symbolized_params(parameters)
|
281
|
-
parameters.inject({}) do |hash,(k,v)|
|
282
|
-
case v
|
283
|
-
when LogStash::Timestamp
|
284
|
-
hash[k.to_sym] = v.time
|
285
|
-
else
|
286
|
-
hash[k.to_sym] = v
|
287
|
-
end
|
288
|
-
hash
|
289
|
-
end
|
290
|
-
end
|
291
|
-
|
292
|
-
private
|
293
|
-
#Stringify row keys and decorate values when necessary
|
294
|
-
def extract_values_from(row)
|
295
|
-
Hash[row.map { |k, v| [k.to_s, decorate_value(v)] }]
|
296
|
-
end
|
297
|
-
|
298
|
-
private
|
299
|
-
def decorate_value(value)
|
300
|
-
if value.is_a?(Time)
|
301
|
-
# transform it to LogStash::Timestamp as required by LS
|
302
|
-
LogStash::Timestamp.new(value)
|
303
|
-
elsif value.is_a?(Date)
|
304
|
-
LogStash::Timestamp.new(value.to_time)
|
305
|
-
elsif value.is_a?(DateTime)
|
306
|
-
# Manual timezone conversion detected.
|
307
|
-
# This is slower, so we put it in as a conditional case.
|
308
|
-
LogStash::Timestamp.new(Time.parse(value.to_s))
|
309
|
-
else
|
310
|
-
value
|
311
|
-
end
|
312
|
-
end
|
313
|
-
end
|