Skip to content

Commit 2b439cb

Browse files
committed
Set application timezone for zoneless datestamps
Fortunately, the Sequel gem already had the necessary functionality. However, it turns out that Sequel _can_ do named timezones, but when you do the date comes back as a DateTime object instead. As a result, this should only use named_timezone support if defined, which should save cycles for those who already have their date fields in UTC, rather than punishing every user. Use string date to test timezone conversion to guarantee similarity to the issue case Fixes #95
1 parent 1a46d7e commit 2b439cb

File tree

4 files changed

+107
-9
lines changed

4 files changed

+107
-9
lines changed

CHANGELOG.md

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
## 2.1.0
22
- [#85](https://github.com/logstash-plugins/logstash-input-jdbc/issues/85) make the jdbc_driver_library accept a list of elements separated by commas as in some situations we might need to load more than one jar/lib.
3+
- [#89](https://github.com/logstash-plugins/logstash-input-jdbc/issues/89) Set application timezone for cases where time fields in data have no timezone.
34

45
## 2.0.5
56
- [#77](https://github.com/logstash-plugins/logstash-input-jdbc/issues/77) Time represented as RubyTime and not as Logstash::Timestamp
@@ -13,7 +14,7 @@
1314
- Added catch-all configuration option for any other options that Sequel lib supports
1415

1516
## 2.0.0
16-
- Plugins were updated to follow the new shutdown semantic, this mainly allows Logstash to instruct input plugins to terminate gracefully,
17+
- Plugins were updated to follow the new shutdown semantic, this mainly allows Logstash to instruct input plugins to terminate gracefully,
1718
instead of using Thread.raise on the plugins' threads. Ref: https://github.com/elastic/logstash/pull/3895
1819
- Dependency on logstash-core update to 2.0
1920

lib/logstash/plugin_mixins/jdbc.rb

Lines changed: 27 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,10 @@
11
# encoding: utf-8
22
# TAKEN FROM WIIBAA
33
require "logstash/config/mixin"
4+
require "time"
5+
require "date"
46

5-
# Tentative of abstracting JDBC logic to a mixin
7+
# Tentative of abstracting JDBC logic to a mixin
68
# for potential reuse in other plugins (input/output)
79
module LogStash::PluginMixins::Jdbc
810

@@ -45,7 +47,7 @@ def setup_jdbc_config
4547
# result-set. The limit size is set with `jdbc_page_size`.
4648
#
4749
# Be aware that ordering is not guaranteed between queries.
48-
config :jdbc_paging_enabled, :validate => :boolean, :default => false
50+
config :jdbc_paging_enabled, :validate => :boolean, :default => false
4951

5052
# JDBC page size
5153
config :jdbc_page_size, :validate => :number, :default => 100000
@@ -65,6 +67,15 @@ def setup_jdbc_config
6567
# The amount of seconds to wait to acquire a connection before raising a PoolTimeoutError (default 5)
6668
config :jdbc_pool_timeout, :validate => :number, :default => 5
6769

70+
# Timezone conversion.
71+
# SQL does not allow for timezone data in timestamp fields. This plugin will automatically
72+
# convert your SQL timestamp fields to Logstash timestamps, in relative UTC time in ISO8601 format.
73+
#
74+
# Using this setting will manually assign a specified timezone offset, instead
75+
# of using the timezone setting of the local machine. You must use a canonical
76+
# timezone, *America/Denver*, for example.
77+
config :jdbc_default_timezone, :validate => :string
78+
6879
# General/Vendor-specific Sequel configuration options.
6980
#
7081
# An example of an optional connection pool configuration
@@ -114,12 +125,12 @@ def prepare_jdbc_connection
114125
require "sequel"
115126
require "sequel/adapters/jdbc"
116127
load_drivers(@jdbc_driver_library.split(",")) if @jdbc_driver_library
117-
128+
118129
begin
119130
Sequel::JDBC.load_driver(@jdbc_driver_class)
120131
rescue Sequel::AdapterNotFound => e
121132
message = if @jdbc_driver_library.nil?
122-
":jdbc_driver_library is not set, are you sure you included
133+
":jdbc_driver_library is not set, are you sure you included
123134
the proper driver client libraries in your classpath?"
124135
else
125136
"Are you sure you've included the correct jdbc driver in :jdbc_driver_library?"
@@ -128,6 +139,10 @@ def prepare_jdbc_connection
128139
end
129140
@database = jdbc_connect()
130141
@database.extension(:pagination)
142+
if @jdbc_default_timezone
143+
@database.extension(:named_timezones)
144+
@database.timezone = @jdbc_default_timezone
145+
end
131146
if @jdbc_validate_connection
132147
@database.extension(:connection_validator)
133148
@database.pool.connection_validation_timeout = @jdbc_validation_timeout
@@ -152,7 +167,7 @@ def close_jdbc_connection
152167
public
153168
def execute_statement(statement, parameters)
154169
success = false
155-
begin
170+
begin
156171
parameters = symbolized_params(parameters)
157172
query = @database[statement, parameters]
158173
@logger.debug? and @logger.debug("Executing JDBC query", :statement => statement, :parameters => parameters, :count => query.count)
@@ -177,9 +192,9 @@ def execute_statement(statement, parameters)
177192
end
178193

179194
# Symbolize parameters keys to use with Sequel
180-
private
195+
private
181196
def symbolized_params(parameters)
182-
parameters.inject({}) do |hash,(k,v)|
197+
parameters.inject({}) do |hash,(k,v)|
183198
case v
184199
when LogStash::Timestamp
185200
hash[k.to_sym] = v.time
@@ -198,9 +213,14 @@ def extract_values_from(row)
198213

199214
private
200215
def decorate_value(value)
216+
201217
if value.is_a?(Time)
202218
# transform it to LogStash::Timestamp as required by LS
203219
LogStash::Timestamp.new(value)
220+
elsif value.is_a?(DateTime)
221+
# Manual timezone conversion detected.
222+
# This is slower, so we put it in as a conditional case.
223+
LogStash::Timestamp.new(Time.parse(value.to_s))
204224
else
205225
value # no-op
206226
end

logstash-input-jdbc.gemspec

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ Gem::Specification.new do |s|
2121
s.add_runtime_dependency "logstash-core", ">= 2.0.0.beta2", "< 3.0.0"
2222
s.add_runtime_dependency 'logstash-codec-plain'
2323
s.add_runtime_dependency 'sequel'
24+
s.add_runtime_dependency 'tzinfo'
2425
s.add_runtime_dependency 'rufus-scheduler'
2526

2627
s.add_development_dependency 'logstash-devutils'

spec/inputs/jdbc_spec.rb

Lines changed: 77 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,10 +5,12 @@
55
require "sequel/adapters/jdbc"
66
require "timecop"
77
require "stud/temporary"
8+
require "time"
9+
require "date"
810

911
describe LogStash::Inputs::Jdbc do
1012
let(:mixin_settings) do
11-
{ "jdbc_user" => ENV['USER'], "jdbc_driver_class" => "org.apache.derby.jdbc.EmbeddedDriver",
13+
{ "jdbc_user" => ENV['USER'], "jdbc_driver_class" => "org.apache.derby.jdbc.EmbeddedDriver",
1214
"jdbc_connection_string" => "jdbc:derby:memory:testdb;create=true"}
1315
end
1416
let(:settings) { {} }
@@ -216,6 +218,80 @@
216218
end
217219
end
218220

221+
context "when fetching time data with jdbc_default_timezone set" do
222+
let(:mixin_settings) do
223+
{ "jdbc_user" => ENV['USER'], "jdbc_driver_class" => "org.apache.derby.jdbc.EmbeddedDriver",
224+
"jdbc_connection_string" => "jdbc:derby:memory:testdb;create=true",
225+
"jdbc_default_timezone" => "America/Chicago"
226+
}
227+
end
228+
229+
let(:settings) do
230+
{
231+
"statement" => "SELECT * from test_table",
232+
}
233+
end
234+
235+
let(:num_rows) { 10 }
236+
237+
before do
238+
stub_const('ENV', ENV.to_hash.merge('TZ' => 'UTC'))
239+
num_rows.times do
240+
db[:test_table].insert(:num => 1, :custom_time => "2015-01-01 12:00:00", :created_at => Time.now.utc)
241+
end
242+
243+
plugin.register
244+
end
245+
246+
after do
247+
plugin.stop
248+
end
249+
250+
it "should convert the time to reflect the timezone " do
251+
plugin.run(queue)
252+
event = queue.pop
253+
# This reflects a 6 hour time difference between UTC and America/Chicago
254+
expect(event["custom_time"].time).to eq(Time.iso8601("2015-01-01T18:00:00Z"))
255+
end
256+
end
257+
258+
context "when fetching time data without jdbc_default_timezone set" do
259+
260+
let(:mixin_settings) do
261+
{ "jdbc_user" => ENV['USER'], "jdbc_driver_class" => "org.apache.derby.jdbc.EmbeddedDriver",
262+
"jdbc_connection_string" => "jdbc:derby:memory:testdb;create=true"
263+
}
264+
end
265+
266+
let(:settings) do
267+
{
268+
"statement" => "SELECT * from test_table",
269+
}
270+
end
271+
272+
let(:num_rows) { 1 }
273+
274+
before do
275+
stub_const('ENV', ENV.to_hash.merge('TZ' => 'UTC'))
276+
num_rows.times do
277+
db.run "INSERT INTO test_table (created_at, num, custom_time) VALUES (TIMESTAMP('2015-01-01 12:00:00'), 1, TIMESTAMP('2015-01-01 12:00:00'))"
278+
end
279+
280+
plugin.register
281+
end
282+
283+
after do
284+
plugin.stop
285+
end
286+
287+
it "should not convert the time to reflect the timezone " do
288+
plugin.run(queue)
289+
event = queue.pop
290+
# With no timezone set, no change should occur
291+
expect(event["custom_time"].time).to eq(Time.iso8601("2015-01-01T12:00:00Z"))
292+
end
293+
end
294+
219295
context "when iteratively running plugin#run" do
220296
let(:settings) do
221297
{"statement" => "SELECT num, created_at FROM test_table WHERE created_at > :sql_last_start"}

0 commit comments

Comments
 (0)