logstash-input-jdbc 4.3.11 → 4.3.12
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- 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
|