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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: d9e1072279ad555d69710d66bc23f5d6af1d5492998c6a2a30edb8dea03bb402
4
- data.tar.gz: 03ca8379b772ef0a4b5d8693fbcee2adbe3c2226922f19e38c2f70e0b02d573e
3
+ metadata.gz: 68b7c5e096c835eea37700e2f7d68d14f61a6c67e5420e2ec3d1fd9a13cfb037
4
+ data.tar.gz: '0977958f3559d7a3e1e0f3c1b89c225e35f5b250fadcd2a5fba880423c6bd48c'
5
5
  SHA512:
6
- metadata.gz: 151670dbc5de220c1c68eef6b723a2c49a1fa93f19efe97a6286c0968309ea53bb57be19e0a46a359c094ce6ce8a69a3cc78213f9884f67fec6477622bc70c94
7
- data.tar.gz: 19929d6d094038af8405cb019de5032cc470ab312ffe3e495f47c193ee066f87fd9fdae7dcb79f8fd6b1a0fc27db1450e876bce8b0437c9e35706059d726b8dd
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
@@ -1,5 +1,5 @@
1
1
  Elasticsearch
2
- Copyright 2012-2015 Elasticsearch
2
+ Copyright 2012-2018 Elasticsearch
3
3
 
4
4
  This product includes software developed by The Apache Software
5
5
  Foundation (http://www.apache.org/).
@@ -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
- class LogStash::Inputs::Jdbc < LogStash::Inputs::Base
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
- @value_tracker = LogStash::PluginMixins::ValueTracking.build_last_value_tracker(self)
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
@@ -1,6 +1,6 @@
1
1
  Gem::Specification.new do |s|
2
2
  s.name = 'logstash-input-jdbc'
3
- s.version = '4.3.11'
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
+
@@ -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.11
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-07-23 00:00:00.000000000 Z
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/value_tracking.rb
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