logstash-filter-jdbc_streaming 1.0.7 → 1.0.9

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 6236e1d0673dff75c590651c8ff5814ecb9a76e4982869e1a331a17101f3a2a5
4
- data.tar.gz: 942058abe67d1a453ad35b787e3767d72913664a8501fa104a38ab3bec1ca8fb
3
+ metadata.gz: 677e0e5ec0f5045d139d29bc159e197d7f771391e303a11f0de12b707e54d224
4
+ data.tar.gz: af5f3e19476f2789a65afd5e5e10faa48a632b667ba0c5ca2ffc030720a23562
5
5
  SHA512:
6
- metadata.gz: dd503c13039572e885d9d8116e4ca64eaa68c3de5a548870228b4179ec4d6708ac1bf4f8e366fc4bbc03e1910f700392eb8ea21ab40ca5a52eda5e71f7aad559
7
- data.tar.gz: c2a6829fb50246a6c67a22b0eedcc67e71e7ba5b290d12bf3880c9c89a67ec5e909734766e6c5337d8772ba26eda33c2e8d80b7c2b13a39f44d0fe03bfa41821
6
+ metadata.gz: 82b751b0170789cf6b27939030976f3c315d53221e82dcc5862ab1cbca43cdf7df2086ee2f9649aa2da469dbbac5a49d058fe6095c47e4f8228a4a6c0ba50c7a
7
+ data.tar.gz: a096d52b59659d6aee46c764567a9131bb077da1e13aed1e492884f10ace4050448f88750e433d689d509e3642286865d756df3e0d6e8713370cd9f6c2d3d2e3
data/CHANGELOG.md CHANGED
@@ -1,5 +1,13 @@
1
+ ## 1.0.9
2
+ - Added support for prepared statements [PR 32](https://github.com/logstash-plugins/logstash-filter-jdbc_streaming/pull/32)
3
+ - Added support for `sequel_opts` to pass options to the 3rd party Sequel library.
4
+
5
+ ## 1.0.8
6
+ - Added support for driver loading in JDK 9+ [Issue 25](https://github.com/logstash-plugins/logstash-filter-jdbc_streaming/issues/25)
7
+ - Added support for multiple driver jars [Issue #21](https://github.com/logstash-plugins/logstash-filter-jdbc_streaming/issues/21)
8
+
1
9
  ## 1.0.7
2
- - Fixed formatting in documentation [#17](https://github.com/logstash-plugins/logstash-filter-jdbc_streaming/pull/17) and [#28](https://github.com/logstash-plugins/logstash-filter-jdbc_streaming/pull/28)
10
+ - Fixed formatting in documentation [#17](https://github.com/logstash-plugins/logstash-filter-jdbc_streaming/pull/17) and [#28](https://github.com/logstash-plugins/logstash-filter-jdbc_streaming/pull/28)
3
11
 
4
12
  ## 1.0.6
5
13
  - Fixes connection leak in pipeline reloads by properly disconnecting on plugin close
@@ -8,7 +16,7 @@
8
16
  - [#11](https://github.com/logstash-plugins/logstash-filter-jdbc_streaming/pull/11) Swap out mysql for postgresql for testing
9
17
 
10
18
  ## 1.0.4
11
- - [JDBC input - #263](https://github.com/logstash-plugins/logstash-input-jdbc/issues/263) Load the driver with the system class loader. Fixes issue loading some JDBC drivers in Logstash 6.2+
19
+ - [JDBC input - #263](https://github.com/logstash-plugins/logstash-input-jdbc/issues/263) Load the driver with the system class loader. Fixes issue loading some JDBC drivers in Logstash 6.2+
12
20
 
13
21
  ## 1.0.3
14
22
  - Update gemspec summary
data/docs/index.asciidoc CHANGED
@@ -42,6 +42,47 @@ filter {
42
42
  }
43
43
  -----
44
44
 
45
+ [id="plugins-{type}s-{plugin}-prepared_statements"]
46
+ ==== Prepared Statements
47
+
48
+ Using server side prepared statements can speed up execution times as the server optimises the query plan and execution.
49
+
50
+ NOTE: Not all JDBC accessible technologies will support prepared statements.
51
+
52
+ With the introduction of Prepared Statement support comes a different code execution path and some new settings. Most of the existing settings are still useful but there are several new settings for Prepared Statements to read up on.
53
+
54
+ Use the boolean setting `use_prepared_statements` to enable this execution mode.
55
+
56
+ Use the `prepared_statement_name` setting to specify a name for the Prepared Statement, this identifies the prepared statement locally and remotely and it should be unique in your config and on the database.
57
+
58
+ Use the `prepared_statement_bind_values` array setting to specify the bind values. Typically, these values are indirectly extracted from your event, i.e. the string in the array refers to a field name in your event. You can also use constant values like numbers or strings but ensure that any string constants (e.g. a locale constant of "en" or "de") is not also an event field name. It is a good idea to use the bracketed field reference syntax for fields and normal strings for constants, e.g. `prepared_statement_bind_values => ["[src_ip]", "tokyo"],`.
59
+
60
+ There are 3 possible parameter schemes. Interpolated, field references and constants. Use interpolation when you are prefixing, suffixing or concatenating field values to create a value that exists in your database, e.g. "%{username}@%{domain}" -> "alice@example.org", "%{distance}km" -> "42km". Use field references for exact field values e.g. "[srcip]" -> "192.168.1.2". Use constants when a database column holds values that slice or categorise a number of similar records e.g. language translations.
61
+
62
+ A boolean setting `prepared_statement_warn_on_constant_usage`, defaulting to true, controls whether you will see a WARN message logged that warns when constants could be missing the bracketed field reference syntax. If you have set your field references and constants correctly you should set `prepared_statement_warn_on_constant_usage` to false. This setting and code checks should be deprecated in a future major Logstash release.
63
+
64
+ The `statement` (or `statement_path`) setting still holds the SQL statement but to use bind variables you must use the `?` character as a placeholder in the exact order found in the `prepared_statement_bind_values` array.
65
+ Some technologies may require connection string properties to be set, see MySQL example below.
66
+
67
+ Example:
68
+ [source,ruby]
69
+ -----
70
+ filter {
71
+ jdbc_streaming {
72
+ jdbc_driver_library => "/path/to/mysql-connector-java-5.1.34-bin.jar"
73
+ jdbc_driver_class => "com.mysql.jdbc.Driver"
74
+ jdbc_connection_string => "jdbc:mysql://localhost:3306/mydatabase?cachePrepStmts=true&prepStmtCacheSize=250&prepStmtCacheSqlLimit=2048&useServerPrepStmts=true"
75
+ jdbc_user => "me"
76
+ jdbc_password => "secret"
77
+ statement => "select * from WORLD.COUNTRY WHERE Code = ?"
78
+ use_prepared_statements => true
79
+ prepared_statement_name => "lookup_country_info"
80
+ prepared_statement_bind_values => ["[country_code]"]
81
+ target => "country_details"
82
+ }
83
+ }
84
+ -----
85
+
45
86
  [id="plugins-{type}s-{plugin}-options"]
46
87
  ==== Jdbc_streaming Filter Configuration Options
47
88
 
@@ -61,11 +102,16 @@ This plugin supports the following configuration options plus the <<plugins-{typ
61
102
  | <<plugins-{type}s-{plugin}-jdbc_validate_connection>> |<<boolean,boolean>>|No
62
103
  | <<plugins-{type}s-{plugin}-jdbc_validation_timeout>> |<<number,number>>|No
63
104
  | <<plugins-{type}s-{plugin}-parameters>> |<<hash,hash>>|No
105
+ | <<plugins-{type}s-{plugin}-prepared_statement_bind_values>> |<<array,array>>|No
106
+ | <<plugins-{type}s-{plugin}-prepared_statement_name>> |<<string,string>>|No
107
+ | <<plugins-{type}s-{plugin}-prepared_statement_warn_on_constant_usage>> |<<boolean,boolean>>|No
108
+ | <<plugins-{type}s-{plugin}-sequel_opts>> |<<hash,hash>>|No
64
109
  | <<plugins-{type}s-{plugin}-statement>> |<<string,string>>|Yes
65
110
  | <<plugins-{type}s-{plugin}-tag_on_default_use>> |<<array,array>>|No
66
111
  | <<plugins-{type}s-{plugin}-tag_on_failure>> |<<array,array>>|No
67
112
  | <<plugins-{type}s-{plugin}-target>> |<<string,string>>|Yes
68
113
  | <<plugins-{type}s-{plugin}-use_cache>> |<<boolean,boolean>>|No
114
+ | <<plugins-{type}s-{plugin}-use_prepared_statements>> |<<boolean,boolean>>|No
69
115
  |=======================================================================
70
116
 
71
117
  Also see <<plugins-{type}s-{plugin}-common-options>> for a list of options supported by all
@@ -74,7 +120,7 @@ filter plugins.
74
120
  &nbsp;
75
121
 
76
122
  [id="plugins-{type}s-{plugin}-cache_expiration"]
77
- ===== `cache_expiration`
123
+ ===== `cache_expiration`
78
124
 
79
125
  * Value type is <<number,number>>
80
126
  * Default value is `5.0`
@@ -90,7 +136,7 @@ any external problem that would cause jdbc errors will not be noticed for the
90
136
  cache_expiration period.
91
137
 
92
138
  [id="plugins-{type}s-{plugin}-cache_size"]
93
- ===== `cache_size`
139
+ ===== `cache_size`
94
140
 
95
141
  * Value type is <<number,number>>
96
142
  * Default value is `500`
@@ -99,7 +145,7 @@ The maximum number of cache entries that will be stored. Defaults to 500 entries
99
145
  The least recently used entry will be evicted.
100
146
 
101
147
  [id="plugins-{type}s-{plugin}-default_hash"]
102
- ===== `default_hash`
148
+ ===== `default_hash`
103
149
 
104
150
  * Value type is <<hash,hash>>
105
151
  * Default value is `{}`
@@ -108,7 +154,7 @@ Define a default object to use when lookup fails to return a matching row.
108
154
  Ensure that the key names of this object match the columns from the statement.
109
155
 
110
156
  [id="plugins-{type}s-{plugin}-jdbc_connection_string"]
111
- ===== `jdbc_connection_string`
157
+ ===== `jdbc_connection_string`
112
158
 
113
159
  * This is a required setting.
114
160
  * Value type is <<string,string>>
@@ -117,7 +163,7 @@ Ensure that the key names of this object match the columns from the statement.
117
163
  JDBC connection string
118
164
 
119
165
  [id="plugins-{type}s-{plugin}-jdbc_driver_class"]
120
- ===== `jdbc_driver_class`
166
+ ===== `jdbc_driver_class`
121
167
 
122
168
  * This is a required setting.
123
169
  * Value type is <<string,string>>
@@ -126,19 +172,15 @@ JDBC connection string
126
172
  JDBC driver class to load, for example "oracle.jdbc.OracleDriver" or "org.apache.derby.jdbc.ClientDriver"
127
173
 
128
174
  [id="plugins-{type}s-{plugin}-jdbc_driver_library"]
129
- ===== `jdbc_driver_library`
175
+ ===== `jdbc_driver_library`
130
176
 
131
177
  * Value type is <<path,path>>
132
178
  * There is no default value for this setting.
133
179
 
134
- Tentative of abstracting JDBC logic to a mixin
135
- for potential reuse in other plugins (input/output).
136
- This method is called when someone includes this module.
137
- Add these methods to the 'base' given.
138
180
  JDBC driver library path to third party driver library.
139
181
 
140
182
  [id="plugins-{type}s-{plugin}-jdbc_password"]
141
- ===== `jdbc_password`
183
+ ===== `jdbc_password`
142
184
 
143
185
  * Value type is <<password,password>>
144
186
  * There is no default value for this setting.
@@ -146,7 +188,7 @@ JDBC driver library path to third party driver library.
146
188
  JDBC password
147
189
 
148
190
  [id="plugins-{type}s-{plugin}-jdbc_user"]
149
- ===== `jdbc_user`
191
+ ===== `jdbc_user`
150
192
 
151
193
  * Value type is <<string,string>>
152
194
  * There is no default value for this setting.
@@ -154,7 +196,7 @@ JDBC password
154
196
  JDBC user
155
197
 
156
198
  [id="plugins-{type}s-{plugin}-jdbc_validate_connection"]
157
- ===== `jdbc_validate_connection`
199
+ ===== `jdbc_validate_connection`
158
200
 
159
201
  * Value type is <<boolean,boolean>>
160
202
  * Default value is `false`
@@ -163,7 +205,7 @@ Connection pool configuration.
163
205
  Validate connection before use.
164
206
 
165
207
  [id="plugins-{type}s-{plugin}-jdbc_validation_timeout"]
166
- ===== `jdbc_validation_timeout`
208
+ ===== `jdbc_validation_timeout`
167
209
 
168
210
  * Value type is <<number,number>>
169
211
  * Default value is `3600`
@@ -172,15 +214,55 @@ Connection pool configuration.
172
214
  How often to validate a connection (in seconds).
173
215
 
174
216
  [id="plugins-{type}s-{plugin}-parameters"]
175
- ===== `parameters`
217
+ ===== `parameters`
176
218
 
177
219
  * Value type is <<hash,hash>>
178
220
  * Default value is `{}`
179
221
 
180
222
  Hash of query parameter, for example `{ "id" => "id_field" }`.
181
223
 
224
+ [id="plugins-{type}s-{plugin}-prepared_statement_bind_values"]
225
+ ===== `prepared_statement_bind_values`
226
+
227
+ * Value type is <<array,array>>
228
+ * Default value is `[]`
229
+
230
+ Array of bind values for the prepared statement. Use field references and constants. See the section on <<plugins-{type}s-{plugin}-prepared_statements,prepared_statements>> for more info.
231
+
232
+ [id="plugins-{type}s-{plugin}-prepared_statement_name"]
233
+ ===== `prepared_statement_name`
234
+
235
+ * Value type is <<string,string>>
236
+ * Default value is `""`
237
+
238
+ Name given to the prepared statement. It must be unique in your config and in the database.
239
+ You need to supply this if `use_prepared_statements` is true.
240
+
241
+ [id="plugins-{type}s-{plugin}-prepared_statement_warn_on_constant_usage"]
242
+ ===== `prepared_statement_warn_on_constant_usage`
243
+
244
+ * Value type is <<boolean,boolean>>
245
+ * Default value is `true`
246
+
247
+ A flag that controls whether a warning is logged if, in `prepared_statement_bind_values`,
248
+ a String constant is detected that might be intended as a field reference.
249
+
250
+ [id="plugins-{type}s-{plugin}-sequel_opts"]
251
+ ===== `sequel_opts`
252
+
253
+ * Value type is <<hash,hash>>
254
+ * Default value is `{}`
255
+
256
+ General/Vendor-specific Sequel configuration options
257
+
258
+ An example of an optional connection pool configuration
259
+ max_connections - The maximum number of connections the connection pool
260
+
261
+ examples of vendor-specific options can be found in this documentation page:
262
+ https://github.com/jeremyevans/sequel/blob/master/doc/opening_databases.rdoc
263
+
182
264
  [id="plugins-{type}s-{plugin}-statement"]
183
- ===== `statement`
265
+ ===== `statement`
184
266
 
185
267
  * This is a required setting.
186
268
  * Value type is <<string,string>>
@@ -190,7 +272,7 @@ Statement to execute.
190
272
  To use parameters, use named parameter syntax, for example "SELECT * FROM MYTABLE WHERE ID = :id".
191
273
 
192
274
  [id="plugins-{type}s-{plugin}-tag_on_default_use"]
193
- ===== `tag_on_default_use`
275
+ ===== `tag_on_default_use`
194
276
 
195
277
  * Value type is <<array,array>>
196
278
  * Default value is `["_jdbcstreamingdefaultsused"]`
@@ -198,7 +280,7 @@ To use parameters, use named parameter syntax, for example "SELECT * FROM MYTABL
198
280
  Append values to the `tags` field if no record was found and default values were used.
199
281
 
200
282
  [id="plugins-{type}s-{plugin}-tag_on_failure"]
201
- ===== `tag_on_failure`
283
+ ===== `tag_on_failure`
202
284
 
203
285
  * Value type is <<array,array>>
204
286
  * Default value is `["_jdbcstreamingfailure"]`
@@ -206,7 +288,7 @@ Append values to the `tags` field if no record was found and default values were
206
288
  Append values to the `tags` field if sql error occurred.
207
289
 
208
290
  [id="plugins-{type}s-{plugin}-target"]
209
- ===== `target`
291
+ ===== `target`
210
292
 
211
293
  * This is a required setting.
212
294
  * Value type is <<string,string>>
@@ -216,14 +298,20 @@ Define the target field to store the extracted result(s).
216
298
  Field is overwritten if exists.
217
299
 
218
300
  [id="plugins-{type}s-{plugin}-use_cache"]
219
- ===== `use_cache`
301
+ ===== `use_cache`
220
302
 
221
303
  * Value type is <<boolean,boolean>>
222
304
  * Default value is `true`
223
305
 
224
306
  Enable or disable caching, boolean true or false. Defaults to true.
225
307
 
308
+ [id="plugins-{type}s-{plugin}-use_prepared_statements"]
309
+ ===== `use_prepared_statements`
310
+
311
+ * Value type is <<boolean,boolean>>
312
+ * Default value is `false`
226
313
 
314
+ When set to `true`, enables prepare statement usage
227
315
 
228
316
  [id="plugins-{type}s-{plugin}-common-options"]
229
317
  include::{include_path}/{type}.asciidoc[]
@@ -2,6 +2,9 @@
2
2
  require "logstash/filters/base"
3
3
  require "logstash/namespace"
4
4
  require "logstash/plugin_mixins/jdbc_streaming"
5
+ require "logstash/plugin_mixins/jdbc_streaming/cache_payload"
6
+ require "logstash/plugin_mixins/jdbc_streaming/statement_handler"
7
+ require "logstash/plugin_mixins/jdbc_streaming/parameter_handler"
5
8
  require "lru_redux"
6
9
 
7
10
  # This filter executes a SQL query and store the result set in the field
@@ -24,49 +27,25 @@ require "lru_redux"
24
27
  # }
25
28
  # }
26
29
  #
30
+ # Prepared Statement Mode example
31
+ #
32
+ # [source,ruby]
33
+ # filter {
34
+ # jdbc_streaming {
35
+ # jdbc_driver_library => "/path/to/mysql-connector-java-5.1.34-bin.jar"
36
+ # jdbc_driver_class => "com.mysql.jdbc.Driver"
37
+ # jdbc_connection_string => ""jdbc:mysql://localhost:3306/mydatabase"
38
+ # jdbc_user => "me"
39
+ # jdbc_password => "secret"
40
+ # statement => "select * from WORLD.COUNTRY WHERE Code = ?"
41
+ # use_prepared_statements => true
42
+ # prepared_statement_name => "get_country_from_code"
43
+ # prepared_statement_bind_values => ["[country_code]"]
44
+ # target => "country_details"
45
+ # }
46
+ # }
47
+ #
27
48
  module LogStash module Filters class JdbcStreaming < LogStash::Filters::Base
28
- class CachePayload
29
- attr_reader :payload
30
- def initialize
31
- @failure = false
32
- @payload = []
33
- end
34
-
35
- def push(data)
36
- @payload << data
37
- end
38
-
39
- def failed!
40
- @failure = true
41
- end
42
-
43
- def failed?
44
- @failure
45
- end
46
-
47
- def empty?
48
- @payload.empty?
49
- end
50
- end
51
-
52
- class RowCache
53
- def initialize(size, ttl)
54
- @cache = ::LruRedux::TTL::ThreadSafeCache.new(size, ttl)
55
- end
56
-
57
- def get(parameters)
58
- @cache.getset(parameters) { yield }
59
- end
60
- end
61
-
62
- class NoCache
63
- def initialize(size, ttl) end
64
-
65
- def get(statement)
66
- yield
67
- end
68
- end
69
-
70
49
  include LogStash::PluginMixins::JdbcStreaming
71
50
 
72
51
  config_name "jdbc_streaming"
@@ -108,16 +87,41 @@ module LogStash module Filters class JdbcStreaming < LogStash::Filters::Base
108
87
  # The least recently used entry will be evicted
109
88
  config :cache_size, :validate => :number, :default => 500
110
89
 
90
+ config :use_prepared_statements, :validate => :boolean, :default => false
91
+ config :prepared_statement_name, :validate => :string, :default => ""
92
+ config :prepared_statement_bind_values, :validate => :array, :default => []
93
+ config :prepared_statement_warn_on_constant_usage, :validate => :boolean, :default => true # deprecate in a future major LS release
94
+
95
+ # Options hash to pass to Sequel
96
+ config :sequel_opts, :validate => :hash, :default => {}
97
+
98
+ attr_reader :prepared_statement_constant_warned # for test verification, remove when warning is deprecated and removed
99
+
111
100
  # ----------------------------------------
112
101
  public
113
102
 
114
103
  def register
115
104
  convert_config_options
116
- prepare_connected_jdbc_cache
105
+ if @use_prepared_statements
106
+ validation_errors = validate_prepared_statement_mode
107
+ unless validation_errors.empty?
108
+ raise(LogStash::ConfigurationError, "Prepared Statement Mode validation errors: " + validation_errors.join(", "))
109
+ end
110
+ else
111
+ # symbolise and wrap value in parameter handler
112
+ unless @parameters.values.all?{|v| v.is_a?(PluginMixins::JdbcStreaming::ParameterHandler)}
113
+ @parameters = parameters.inject({}) do |hash,(k,value)|
114
+ hash[k.to_sym] = PluginMixins::JdbcStreaming::ParameterHandler.build_parameter_handler(value)
115
+ hash
116
+ end
117
+ end
118
+ end
119
+ @statement_handler = LogStash::PluginMixins::JdbcStreaming::StatementHandler.build_statement_handler(self)
120
+ prepare_jdbc_connection
117
121
  end
118
122
 
119
123
  def filter(event)
120
- result = cache_lookup(event) # should return a JdbcCachePayload
124
+ result = @statement_handler.cache_lookup(@database, event) # should return a CachePayload instance
121
125
 
122
126
  if result.failed?
123
127
  tag_failure(event)
@@ -140,35 +144,6 @@ module LogStash module Filters class JdbcStreaming < LogStash::Filters::Base
140
144
  # ----------------------------------------
141
145
  private
142
146
 
143
- def cache_lookup(event)
144
- params = prepare_parameters_from_event(event)
145
- @cache.get(params) do
146
- result = CachePayload.new
147
- begin
148
- query = @database[@statement, params] # returns a dataset
149
- @logger.debug? && @logger.debug("Executing JDBC query", :statement => @statement, :parameters => params)
150
- query.all do |row|
151
- result.push row.inject({}){|hash,(k,v)| hash[k.to_s] = v; hash} #Stringify row keys
152
- end
153
- rescue ::Sequel::Error => e
154
- # all sequel errors are a subclass of this, let all other standard or runtime errors bubble up
155
- result.failed!
156
- @logger.warn? && @logger.warn("Exception when executing JDBC query", :exception => e)
157
- end
158
- # if either of: no records or a Sequel exception occurs the payload is
159
- # empty and the default can be substituted later.
160
- result
161
- end
162
- end
163
-
164
- def prepare_parameters_from_event(event)
165
- @symbol_parameters.inject({}) do |hash,(k,v)|
166
- value = event.get(event.sprintf(v))
167
- hash[k] = value.is_a?(::LogStash::Timestamp) ? value.time : value
168
- hash
169
- end
170
- end
171
-
172
147
  def tag_failure(event)
173
148
  @tag_on_failure.each do |tag|
174
149
  event.tag(tag)
@@ -190,13 +165,32 @@ module LogStash module Filters class JdbcStreaming < LogStash::Filters::Base
190
165
  def convert_config_options
191
166
  # create these object once they will be cloned for every filter call anyway,
192
167
  # lets not create a new object for each
193
- @symbol_parameters = @parameters.inject({}) {|hash,(k,v)| hash[k.to_sym] = v ; hash }
194
168
  @default_array = [@default_hash]
195
169
  end
196
170
 
197
- def prepare_connected_jdbc_cache
198
- klass = @use_cache ? RowCache : NoCache
199
- @cache = klass.new(@cache_size, @cache_expiration)
200
- prepare_jdbc_connection
171
+ def validate_prepared_statement_mode
172
+ @prepared_statement_constant_warned = false
173
+ error_messages = []
174
+ if @prepared_statement_name.empty?
175
+ error_messages << "must provide a name for the Prepared Statement, it must be unique for the db session"
176
+ end
177
+ if @statement.count("?") != @prepared_statement_bind_values.size
178
+ # mismatch in number of bind value elements to placeholder characters
179
+ error_messages << "there is a mismatch between the number of statement `?` placeholders and :prepared_statement_bind_values array setting elements"
180
+ end
181
+ unless @prepared_statement_bind_values.all?{|v| v.is_a?(PluginMixins::JdbcStreaming::ParameterHandler)}
182
+ @prepared_statement_bind_values = prepared_statement_bind_values.map do |value|
183
+ ParameterHandler.build_bind_value_handler(value)
184
+ end
185
+ end
186
+ if prepared_statement_warn_on_constant_usage
187
+ warnables = @prepared_statement_bind_values.select {|handler| handler.is_a?(PluginMixins::JdbcStreaming::ConstantParameter) && handler.given_value.is_a?(String)}
188
+ unless warnables.empty?
189
+ @prepared_statement_constant_warned = true
190
+ msg = "When using prepared statements, the following `prepared_statement_bind_values` will be treated as constants, if you intend them to be field references please use the square bracket field reference syntax e.g. '[field]'"
191
+ logger.warn(msg, :constants => warnables)
192
+ end
193
+ end
194
+ error_messages
201
195
  end
202
- end end end # class LogStash::Filters::Jdbc
196
+ end end end # class LogStash::Filters::JdbcStreaming
@@ -0,0 +1,28 @@
1
+ # encoding: utf-8
2
+
3
+ module LogStash module PluginMixins module JdbcStreaming
4
+ class CachePayload
5
+ attr_reader :payload
6
+
7
+ def initialize
8
+ @failure = false
9
+ @payload = []
10
+ end
11
+
12
+ def push(data)
13
+ @payload << data
14
+ end
15
+
16
+ def failed!
17
+ @failure = true
18
+ end
19
+
20
+ def failed?
21
+ @failure
22
+ end
23
+
24
+ def empty?
25
+ @payload.empty?
26
+ end
27
+ end
28
+ end end end
@@ -0,0 +1,64 @@
1
+ # encoding: utf-8
2
+
3
+ module LogStash module PluginMixins module JdbcStreaming
4
+ class ParameterHandler
5
+
6
+ def self.build_parameter_handler(given_value)
7
+ # does it really make sense to deal with normal parameters differently?
8
+ handler = FieldParameter.new(given_value)
9
+ return handler unless given_value.is_a?(String)
10
+
11
+ first_percent_curly = given_value.index("%{")
12
+ if first_percent_curly && given_value.index("}", first_percent_curly)
13
+ return InterpolatedParameter.new(given_value)
14
+ end
15
+
16
+ handler
17
+ end
18
+
19
+ def self.build_bind_value_handler(given_value)
20
+ handler = ConstantParameter.new(given_value)
21
+
22
+ return handler unless given_value.is_a?(String) # allow non String constants
23
+
24
+ first_percent_curly = given_value.index("%{")
25
+ if first_percent_curly && given_value.index("}", first_percent_curly)
26
+ return InterpolatedParameter.new(given_value)
27
+ end
28
+
29
+ if given_value =~ /\A\s*\[[^\]]+\]\s*\z/
30
+ return FieldParameter.new(given_value)
31
+ end
32
+
33
+ handler
34
+ end
35
+
36
+ attr_reader :given_value
37
+
38
+ def initialize(given_value)
39
+ @given_value = given_value
40
+ end
41
+
42
+ def extract_from(event)
43
+ # override in subclass
44
+ end
45
+ end
46
+
47
+ class InterpolatedParameter < ParameterHandler
48
+ def extract_from(event)
49
+ event.sprintf(@given_value)
50
+ end
51
+ end
52
+
53
+ class FieldParameter < ParameterHandler
54
+ def extract_from(event)
55
+ event.get(@given_value)
56
+ end
57
+ end
58
+
59
+ class ConstantParameter < ParameterHandler
60
+ def extract_from(event)
61
+ @given_value
62
+ end
63
+ end
64
+ end end end
@@ -0,0 +1,143 @@
1
+ # encoding: utf-8
2
+ require "logstash/util/loggable"
3
+
4
+ module LogStash module PluginMixins module JdbcStreaming
5
+ # so as to not clash with the class of the same name and function in the jdbc input
6
+ # this is in the `module JdbcStreaming` namespace
7
+ # this duplication can be removed in a universal plugin
8
+
9
+ class StatementHandler
10
+ def self.build_statement_handler(plugin)
11
+ klass = plugin.use_prepared_statements ? PreparedStatementHandler : NormalStatementHandler
12
+ klass.new(plugin)
13
+ end
14
+
15
+ attr_reader :statement, :parameters, :cache
16
+
17
+ def initialize(plugin)
18
+ @statement = plugin.statement
19
+ klass = plugin.use_cache ? RowCache : NoCache
20
+ @cache = klass.new(plugin.cache_size, plugin.cache_expiration)
21
+ post_init(plugin)
22
+ end
23
+
24
+ # Get from cache or performs remote lookup and saves to cache
25
+ # @param db [Sequel::Database]
26
+ # @param event [LogStash::Event]
27
+ # @returnparam [CachePayload]
28
+ def cache_lookup(db, event)
29
+ # override in subclass
30
+ end
31
+
32
+ private
33
+
34
+ def common_cache_lookup(db, event)
35
+ params = prepare_parameters_from_event(event)
36
+ @cache.get(params) do
37
+ result = CachePayload.new
38
+ begin
39
+ logger.debug? && logger.debug("Executing JDBC query", :statement => statement, :parameters => params)
40
+ execute_extract_records(db, params, result)
41
+ rescue ::Sequel::Error => e
42
+ # all sequel errors are a subclass of this, let all other standard or runtime errors bubble up
43
+ result.failed!
44
+ logger.warn? && logger.warn("Exception when executing JDBC query", :statement => statement, :parameters => params, :exception => e)
45
+ end
46
+ # if either of: no records or a Sequel exception occurs the payload is
47
+ # empty and the default can be substituted later.
48
+ result
49
+ end
50
+ end
51
+
52
+ def execute_extract_records(db, params, result)
53
+ # override in subclass
54
+ end
55
+
56
+ def post_init(plugin)
57
+ # override in subclass, if needed
58
+ end
59
+
60
+ def prepare_parameters_from_event(event)
61
+ @parameters.inject({}) do |hash, (k, parameter_handler)|
62
+ # defer to appropriate parameter handler
63
+ value = parameter_handler.extract_from(event)
64
+ hash[k] = value.is_a?(::LogStash::Timestamp) ? value.time : value
65
+ hash
66
+ end
67
+ end
68
+ end
69
+
70
+ class NormalStatementHandler < StatementHandler
71
+ include LogStash::Util::Loggable
72
+
73
+ # Get from cache or performs remote lookup and saves to cache
74
+ # @param db [Sequel::Database]
75
+ # @param event [LogStash::Event]
76
+ # @returnparam [CachePayload]
77
+ def cache_lookup(db, event)
78
+ common_cache_lookup(db, event)
79
+ end
80
+
81
+ private
82
+
83
+ def execute_extract_records(db, params, result)
84
+ dataset = db[statement, params] # returns a Sequel dataset
85
+ dataset.all do |row|
86
+ result.push row.inject({}){|hash,(k,v)| hash[k.to_s] = v; hash} # Stringify row keys
87
+ end
88
+ end
89
+
90
+ def post_init(plugin)
91
+ @parameters = plugin.parameters
92
+ end
93
+ end
94
+
95
+ class PreparedStatementHandler < StatementHandler
96
+ include LogStash::Util::Loggable
97
+ attr_reader :name, :bind_values_array, :statement_prepared, :prepared
98
+
99
+ # Get from cache or performs remote lookup and saves to cache
100
+ # @param db [Sequel::Database]
101
+ # @param event [LogStash::Event]
102
+ # @returnparam [CachePayload]
103
+ def cache_lookup(db, event)
104
+ build_prepared_statement(db)
105
+ common_cache_lookup(db, event)
106
+ end
107
+
108
+ private
109
+
110
+ def execute_extract_records(db, params, result)
111
+ records = db.call(name, params) # returns an array of hashes
112
+ records.each do |row|
113
+ result.push row.inject({}){|hash,(k,v)| hash[k.to_s] = v; hash} #Stringify row keys
114
+ end
115
+ end
116
+
117
+ def post_init(plugin)
118
+ @name = plugin.prepared_statement_name.to_sym
119
+ @bind_values_array = plugin.prepared_statement_bind_values
120
+ @statement_prepared = Concurrent::AtomicBoolean.new(false)
121
+ @parameters = create_bind_values_hash
122
+ end
123
+
124
+ def build_prepared_statement(db)
125
+ # create prepared statement on first use
126
+ if statement_prepared.false?
127
+ prepended = parameters.keys.map{|v| v.to_s.prepend("$").to_sym}
128
+ @prepared = db[statement, *prepended].prepare(:select, name)
129
+ statement_prepared.make_true
130
+ end
131
+ # make sure the Sequel database instance has the prepared statement
132
+ if db.prepared_statement(name).nil?
133
+ db.set_prepared_statement(name, prepared)
134
+ end
135
+ end
136
+
137
+ def create_bind_values_hash
138
+ hash = {}
139
+ bind_values_array.each_with_index {|v,i| hash[:"p#{i}"] = v}
140
+ hash
141
+ end
142
+ end
143
+ end end end
@@ -0,0 +1,46 @@
1
+ # encoding: utf-8
2
+
3
+ module LogStash module PluginMixins module JdbcStreaming
4
+ class WrappedDriver
5
+ java_implements java.sql.Driver
6
+
7
+ def initialize(drv)
8
+ @driver = drv
9
+ end
10
+
11
+ java_signature 'boolean acceptsURL(String u) throws SQLException'
12
+ def accepts_url(u)
13
+ @driver.accepts_url(u)
14
+ end
15
+
16
+ java_signature 'Connection connect(String u, Properties p)'
17
+ def connect(url, props)
18
+ @driver.connect(url, props)
19
+ end
20
+
21
+ java_signature 'int getMajorVersion()'
22
+ def get_major_version()
23
+ @driver.get_major_version()
24
+ end
25
+
26
+ java_signature 'int getMinorVersion()'
27
+ def get_minor_version()
28
+ @driver.get_minor_version()
29
+ end
30
+
31
+ java_signature 'DriverPropertyInfo[] getPropertyInfo(String u, Properties p)'
32
+ def get_property_info(url, props)
33
+ @driver.get_property_info(url, props)
34
+ end
35
+
36
+ java_signature 'boolean jdbcCompliant()'
37
+ def jdbc_compliant()
38
+ @driver.jdbc_compliant
39
+ end
40
+
41
+ java_signature 'Logger getParentLogger() throws SQLFeatureNotSupportedException'
42
+ def get_parent_logger()
43
+ @driver.get_parent_logger
44
+ end
45
+ end
46
+ end end end
@@ -1,9 +1,27 @@
1
1
  # encoding: utf-8
2
2
  require "logstash/config/mixin"
3
+ require_relative "jdbc_streaming/wrapped_driver"
3
4
 
4
5
  # Tentative of abstracting JDBC logic to a mixin
5
6
  # for potential reuse in other plugins (input/output)
6
7
  module LogStash module PluginMixins module JdbcStreaming
8
+ class RowCache
9
+ def initialize(size, ttl)
10
+ @cache = ::LruRedux::TTL::ThreadSafeCache.new(size, ttl)
11
+ end
12
+
13
+ def get(parameters)
14
+ @cache.getset(parameters) { yield }
15
+ end
16
+ end
17
+
18
+ class NoCache
19
+ def initialize(size, ttl) end
20
+
21
+ def get(statement)
22
+ yield
23
+ end
24
+ end
7
25
 
8
26
  # This method is called when someone includes this module
9
27
  def self.included(base)
@@ -38,19 +56,46 @@ module LogStash module PluginMixins module JdbcStreaming
38
56
  config :jdbc_validation_timeout, :validate => :number, :default => 3600
39
57
  end
40
58
 
59
+ private
60
+
61
+ def load_drivers
62
+ return if @jdbc_driver_library.nil? || @jdbc_driver_library.empty?
63
+ driver_jars = @jdbc_driver_library.split(",")
64
+
65
+ # Needed for JDK 11 as the DriverManager has a different ClassLoader than Logstash
66
+ urls = java.net.URL[driver_jars.length].new
67
+ driver_jars.each_with_index do |driver, idx|
68
+ urls[idx] = java.io.File.new(driver).toURI().toURL()
69
+ end
70
+ ucl = java.net.URLClassLoader.new_instance(urls)
71
+ begin
72
+ klass = java.lang.Class.forName(@jdbc_driver_class.to_java(:string), true, ucl);
73
+ rescue Java::JavaLang::ClassNotFoundException => e
74
+ raise LogStash::Error, "Unable to find driver class via URLClassLoader in given driver jars: #{@jdbc_driver_class}"
75
+ end
76
+ begin
77
+ driver = klass.getConstructor().newInstance();
78
+ java.sql.DriverManager.register_driver(WrappedDriver.new(driver.to_java(java.sql.Driver)).to_java(java.sql.Driver))
79
+ rescue Java::JavaSql::SQLException => e
80
+ raise LogStash::Error, "Unable to register driver with java.sql.DriverManager using WrappedDriver: #{@jdbc_driver_class}"
81
+ end
82
+
83
+ end
84
+
41
85
  public
42
86
  def prepare_jdbc_connection
43
87
  require "sequel"
44
88
  require "sequel/adapters/jdbc"
45
89
  require "java"
46
90
 
47
- if @jdbc_driver_library
48
- class_loader = java.lang.ClassLoader.getSystemClassLoader().to_java(java.net.URLClassLoader)
49
- class_loader.add_url(java.io.File.new(@jdbc_driver_library).toURI().toURL())
50
- end
91
+ load_drivers
92
+
93
+ @sequel_opts_symbols = @sequel_opts.inject({}) {|hash, (k,v)| hash[k.to_sym] = v; hash}
94
+ @sequel_opts_symbols[:user] = @jdbc_user unless @jdbc_user.nil? || @jdbc_user.empty?
95
+ @sequel_opts_symbols[:password] = @jdbc_password.value unless @jdbc_password.nil?
51
96
 
52
97
  Sequel::JDBC.load_driver(@jdbc_driver_class)
53
- @database = Sequel.connect(@jdbc_connection_string, :user=> @jdbc_user, :password=> @jdbc_password.nil? ? nil : @jdbc_password.value)
98
+ @database = Sequel.connect(@jdbc_connection_string, @sequel_opts_symbols)
54
99
  if @jdbc_validate_connection
55
100
  @database.extension(:connection_validator)
56
101
  @database.pool.connection_validation_timeout = @jdbc_validation_timeout
@@ -1,6 +1,6 @@
1
1
  Gem::Specification.new do |s|
2
2
  s.name = 'logstash-filter-jdbc_streaming'
3
- s.version = '1.0.7'
3
+ s.version = '1.0.9'
4
4
  s.licenses = ['Apache License (2.0)']
5
5
  s.summary = "Enrich events with your database 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/plugin install gemname. This gem is not a stand-alone program"
@@ -27,127 +27,91 @@ module LogStash module Filters
27
27
  let(:cache_expiration) { 3.0 }
28
28
  let(:use_cache) { true }
29
29
  let(:cache_size) { 10 }
30
- let(:statement) { "SELECT name, location FROM reference_table WHERE ip = :ip" }
31
- let(:settings) do
32
- {
33
- "statement" => statement,
34
- "parameters" => {"ip" => "ip"},
35
- "target" => "server",
36
- "use_cache" => use_cache,
37
- "cache_expiration" => cache_expiration,
38
- "cache_size" => cache_size,
39
- "tag_on_failure" => ["lookup_failed"],
40
- "tag_on_default_use" => ["default_used_instead"],
41
- "default_hash" => {"name" => "unknown", "location" => "unknown"}
42
- }
43
- end
44
30
 
45
31
  before :each do
46
32
  db.create_table :reference_table do
47
- String :ip
33
+ String :ip
48
34
  String :name
49
35
  String :location
36
+ Integer :gcode
50
37
  end
51
- db[:reference_table].insert(:ip => "10.1.1.1", :name => "ldn-server-1", :location => "LDN-2-3-4")
52
- db[:reference_table].insert(:ip => "10.2.1.1", :name => "nyc-server-1", :location => "NYC-5-2-8")
53
- db[:reference_table].insert(:ip => "10.3.1.1", :name => "mv-server-1", :location => "MV-9-6-4")
54
- plugin.register
38
+ db[:reference_table].insert(:ip => "10.1.1.1", :name => "ldn-server-1", :location => "LDN-2-3-4", :gcode => 3)
39
+ db[:reference_table].insert(:ip => "10.2.1.1", :name => "nyc-server-1", :location => "NYC-5-2-8", :gcode => 1)
40
+ db[:reference_table].insert(:ip => "10.3.1.1", :name => "mv-server-1", :location => "MV-9-6-4", :gcode => 1)
41
+ db[:reference_table].insert(:ip => "10.4.1.1", :name => "sf-server-1", :location => "SF-9-5-4", :gcode => 1)
42
+ db[:reference_table].insert(:ip => "10.4.1.1", :name => "mtl-server-1", :location => "MTL-9-3-4", :gcode => 2)
55
43
  end
56
44
 
57
45
  after :each do
58
46
  db.drop_table(:reference_table)
59
47
  end
60
48
 
61
- describe "found record - uses row" do
62
- let(:ipaddr) { "10.1.1.1" }
63
-
64
- it "fills in the target" do
65
- plugin.filter(event)
66
- expect(event.get("server")).to eq([{"name" => "ldn-server-1", "location" => "LDN-2-3-4"}])
67
- expect(event.get("tags") || []).not_to include("lookup_failed")
68
- expect(event.get("tags") || []).not_to include("default_used_instead")
49
+ context "Normal Mode" do
50
+ before :each do
51
+ plugin.register
69
52
  end
70
- end
71
-
72
- describe "missing record - uses default" do
73
- let(:ipaddr) { "192.168.1.1" }
74
53
 
75
- it "fills in the target with the default" do
76
- plugin.filter(event)
77
- expect(event.get("server")).to eq([{"name" => "unknown", "location" => "unknown"}])
78
- expect(event.get("tags") & ["lookup_failed", "default_used_instead"]).to eq(["default_used_instead"])
79
- end
80
- end
81
-
82
- describe "database error - uses default" do
83
- let(:ipaddr) { "10.1.1.1" }
84
- let(:statement) { "SELECT name, location FROM reference_table WHERE ip = :address" }
85
- it "fills in the target with the default" do
86
- plugin.filter(event)
87
- expect(event.get("server")).to eq([{"name" => "unknown", "location" => "unknown"}])
88
- expect(event.get("tags") & ["lookup_failed", "default_used_instead"]).to eq(["lookup_failed", "default_used_instead"])
89
- end
90
- end
91
-
92
- context "when fetching from cache" do
93
- let(:plugin) { TestJdbcStreaming.new(mixin_settings.merge(settings)) }
94
- let(:events) do
95
- 5.times.map{|i| ::LogStash::Event.new("message" => "some other text #{i}", "ip" => ipaddr) }
96
- end
97
- let(:call_count) { 1 }
98
- before(:each) do
99
- expect(plugin.database).to receive(:[]).exactly(call_count).times.and_call_original
100
- plugin.filter(event)
54
+ let(:statement) { "SELECT name, location FROM reference_table WHERE ip = :ip" }
55
+ let(:settings) do
56
+ {
57
+ "statement" => statement,
58
+ "parameters" => {"ip" => "ip"},
59
+ "target" => "server",
60
+ "use_cache" => use_cache,
61
+ "cache_expiration" => cache_expiration,
62
+ "cache_size" => cache_size,
63
+ "tag_on_failure" => ["lookup_failed"],
64
+ "tag_on_default_use" => ["default_used_instead"],
65
+ "default_hash" => {"name" => "unknown", "location" => "unknown"},
66
+ "sequel_opts" => {"pool_timeout" => 600}
67
+ }
101
68
  end
102
69
 
103
- describe "found record - caches row" do
70
+ describe "found record - uses row" do
104
71
  let(:ipaddr) { "10.1.1.1" }
105
- it "calls the database once then uses the cache" do
72
+
73
+ it "fills in the target" do
74
+ plugin.filter(event)
106
75
  expect(event.get("server")).to eq([{"name" => "ldn-server-1", "location" => "LDN-2-3-4"}])
107
76
  expect(event.get("tags") || []).not_to include("lookup_failed")
108
77
  expect(event.get("tags") || []).not_to include("default_used_instead")
109
- events.each do |evt|
110
- plugin.filter(evt)
111
- expect(evt.get("server")).to eq([{"name" => "ldn-server-1", "location" => "LDN-2-3-4"}])
112
- end
113
78
  end
114
79
  end
115
80
 
116
81
  describe "missing record - uses default" do
117
- let(:ipaddr) { "10.10.1.1" }
118
- it "calls the database once then uses the cache" do
82
+ let(:ipaddr) { "192.168.1.1" }
83
+
84
+ it "fills in the target with the default" do
85
+ plugin.filter(event)
119
86
  expect(event.get("server")).to eq([{"name" => "unknown", "location" => "unknown"}])
120
87
  expect(event.get("tags") & ["lookup_failed", "default_used_instead"]).to eq(["default_used_instead"])
121
- events.each do |evt|
122
- plugin.filter(evt)
123
- expect(evt.get("server")).to eq([{"name" => "unknown", "location" => "unknown"}])
124
- end
125
88
  end
126
89
  end
127
90
 
128
- context "extremely small cache expiration" do
129
- describe "found record - cache always expires" do
130
- let(:ipaddr) { "10.1.1.1" }
131
- let(:call_count) { 6 }
132
- let(:cache_expiration) { 0.0000001 }
133
- it "calls the database each time because cache entry expired" do
134
- expect(event.get("server")).to eq([{"name" => "ldn-server-1", "location" => "LDN-2-3-4"}])
135
- expect(event.get("tags") || []).not_to include("lookup_failed")
136
- expect(event.get("tags") || []).not_to include("default_used_instead")
137
- events.each do |evt|
138
- plugin.filter(evt)
139
- expect(evt.get("server")).to eq([{"name" => "ldn-server-1", "location" => "LDN-2-3-4"}])
140
- end
141
- end
91
+ describe "database error - uses default" do
92
+ let(:ipaddr) { "10.1.1.1" }
93
+ let(:statement) { "SELECT name, location FROM reference_table WHERE ip = :address" }
94
+ it "fills in the target with the default" do
95
+ plugin.filter(event)
96
+ expect(event.get("server")).to eq([{"name" => "unknown", "location" => "unknown"}])
97
+ expect(event.get("tags") & ["lookup_failed", "default_used_instead"]).to eq(["lookup_failed", "default_used_instead"])
142
98
  end
143
99
  end
144
100
 
145
- context "when cache is disabled" do
146
- let(:call_count) { 6 }
147
- let(:use_cache) { false }
148
- describe "database is always called" do
101
+ context "when fetching from cache" do
102
+ let(:plugin) { TestJdbcStreaming.new(mixin_settings.merge(settings)) }
103
+ let(:events) do
104
+ 5.times.map{|i| ::LogStash::Event.new("message" => "some other text #{i}", "ip" => ipaddr) }
105
+ end
106
+ let(:call_count) { 1 }
107
+ before(:each) do
108
+ expect(plugin.database).to receive(:[]).exactly(call_count).times.and_call_original
109
+ plugin.filter(event)
110
+ end
111
+
112
+ describe "found record - caches row" do
149
113
  let(:ipaddr) { "10.1.1.1" }
150
- it "calls the database each time" do
114
+ it "calls the database once then uses the cache" do
151
115
  expect(event.get("server")).to eq([{"name" => "ldn-server-1", "location" => "LDN-2-3-4"}])
152
116
  expect(event.get("tags") || []).not_to include("lookup_failed")
153
117
  expect(event.get("tags") || []).not_to include("default_used_instead")
@@ -158,9 +122,9 @@ module LogStash module Filters
158
122
  end
159
123
  end
160
124
 
161
- describe "database is always called but record is missing and default is used" do
162
- let(:ipaddr) { "10.11.1.1" }
163
- it "calls the database each time" do
125
+ describe "missing record - uses default" do
126
+ let(:ipaddr) { "10.10.1.1" }
127
+ it "calls the database once then uses the cache" do
164
128
  expect(event.get("server")).to eq([{"name" => "unknown", "location" => "unknown"}])
165
129
  expect(event.get("tags") & ["lookup_failed", "default_used_instead"]).to eq(["default_used_instead"])
166
130
  events.each do |evt|
@@ -169,6 +133,113 @@ module LogStash module Filters
169
133
  end
170
134
  end
171
135
  end
136
+
137
+ context "extremely small cache expiration" do
138
+ describe "found record - cache always expires" do
139
+ let(:ipaddr) { "10.1.1.1" }
140
+ let(:call_count) { 6 }
141
+ let(:cache_expiration) { 0.0000001 }
142
+ it "calls the database each time because cache entry expired" do
143
+ expect(event.get("server")).to eq([{"name" => "ldn-server-1", "location" => "LDN-2-3-4"}])
144
+ expect(event.get("tags") || []).not_to include("lookup_failed")
145
+ expect(event.get("tags") || []).not_to include("default_used_instead")
146
+ events.each do |evt|
147
+ plugin.filter(evt)
148
+ expect(evt.get("server")).to eq([{"name" => "ldn-server-1", "location" => "LDN-2-3-4"}])
149
+ end
150
+ end
151
+ end
152
+ end
153
+
154
+ context "when cache is disabled" do
155
+ let(:call_count) { 6 }
156
+ let(:use_cache) { false }
157
+ describe "database is always called" do
158
+ let(:ipaddr) { "10.1.1.1" }
159
+ it "calls the database each time" do
160
+ expect(event.get("server")).to eq([{"name" => "ldn-server-1", "location" => "LDN-2-3-4"}])
161
+ expect(event.get("tags") || []).not_to include("lookup_failed")
162
+ expect(event.get("tags") || []).not_to include("default_used_instead")
163
+ events.each do |evt|
164
+ plugin.filter(evt)
165
+ expect(evt.get("server")).to eq([{"name" => "ldn-server-1", "location" => "LDN-2-3-4"}])
166
+ end
167
+ end
168
+ end
169
+
170
+ describe "database is always called but record is missing and default is used" do
171
+ let(:ipaddr) { "10.11.1.1" }
172
+ it "calls the database each time" do
173
+ expect(event.get("server")).to eq([{"name" => "unknown", "location" => "unknown"}])
174
+ expect(event.get("tags") & ["lookup_failed", "default_used_instead"]).to eq(["default_used_instead"])
175
+ events.each do |evt|
176
+ plugin.filter(evt)
177
+ expect(evt.get("server")).to eq([{"name" => "unknown", "location" => "unknown"}])
178
+ end
179
+ end
180
+ end
181
+ end
182
+ end
183
+ end
184
+
185
+ context "Prepared Statement Mode" do
186
+ let(:statement) { "SELECT name, location FROM reference_table WHERE (ip = ?) AND (gcode = ?)" }
187
+ let(:settings) do
188
+ {
189
+ "statement" => statement,
190
+ "use_prepared_statements" => true,
191
+ "prepared_statement_name" => "lookup_ip",
192
+ "prepared_statement_bind_values" => ["[ip]", 2],
193
+ "target" => "server",
194
+ "use_cache" => use_cache,
195
+ "cache_expiration" => cache_expiration,
196
+ "cache_size" => cache_size,
197
+ "tag_on_failure" => ["lookup_failed"],
198
+ "tag_on_default_use" => ["default_used_instead"],
199
+ "default_hash" => {"name" => "unknown", "location" => "unknown"},
200
+ "sequel_opts" => {"pool_timeout" => 600}
201
+ }
202
+ end
203
+
204
+ describe "using one variable and one constant, found record - uses row" do
205
+ let(:ipaddr) { "10.4.1.1" }
206
+
207
+ it "fills in the target" do
208
+ plugin.register
209
+ expect(plugin.prepared_statement_constant_warned).to be_falsey
210
+ plugin.filter(event)
211
+ expect(event.get("server")).to eq([{"name" => "mtl-server-1", "location" => "MTL-9-3-4"}])
212
+ expect(event.get("tags") || []).not_to include("lookup_failed")
213
+ expect(event.get("tags") || []).not_to include("default_used_instead")
214
+ end
215
+ end
216
+
217
+ describe "fails empty name validation" do
218
+ before :each do
219
+ settings["prepared_statement_name"] = ""
220
+ end
221
+ it "should fail to register" do
222
+ expect{ plugin.register }.to raise_error(LogStash::ConfigurationError)
223
+ end
224
+ end
225
+
226
+ describe "fails parameter mismatch validation" do
227
+ before :each do
228
+ settings["prepared_statement_bind_values"] = ["[ip]"]
229
+ end
230
+ it "should fail to register" do
231
+ expect{ plugin.register }.to raise_error(LogStash::ConfigurationError)
232
+ end
233
+ end
234
+
235
+ describe "warns on constant usage" do
236
+ before :each do
237
+ settings["prepared_statement_bind_values"] = ["ip", 2]
238
+ end
239
+ it "should set the warning logged flag" do
240
+ plugin.register
241
+ expect(plugin.prepared_statement_constant_warned).to be_truthy
242
+ end
172
243
  end
173
244
  end
174
245
  end
@@ -273,5 +344,6 @@ module LogStash module Filters
273
344
  expect(subject.get('new_field')).to eq([{"1" => 'from_database'}])
274
345
  end
275
346
  end
347
+
276
348
  end
277
349
  end end
@@ -14,13 +14,18 @@ module LogStash module Filters
14
14
  ::Jdbc::Postgres.load_driver
15
15
 
16
16
  ENV["TZ"] = "Etc/UTC"
17
+
18
+ # For Travis and CI based on docker, we source from ENV
19
+ jdbc_connection_string = ENV.fetch("PG_CONNECTION_STRING",
20
+ "jdbc:postgresql://localhost:5432") + "/jdbc_streaming_db?user=postgres"
21
+
17
22
  let(:mixin_settings) do
18
- { "jdbc_user" => "postgres", "jdbc_driver_class" => "org.postgresql.Driver",
19
- "jdbc_connection_string" => "jdbc:postgresql://localhost/jdbc_streaming_db?user=postgres"}
23
+ { "jdbc_driver_class" => "org.postgresql.Driver",
24
+ "jdbc_connection_string" => jdbc_connection_string
25
+ }
20
26
  end
21
- let(:settings) { {} }
22
27
  let(:plugin) { JdbcStreaming.new(mixin_settings.merge(settings)) }
23
- let (:db) do
28
+ let(:db) do
24
29
  ::Sequel.connect(mixin_settings['jdbc_connection_string'])
25
30
  end
26
31
  let(:event) { ::LogStash::Event.new("message" => "some text", "ip" => ipaddr) }
@@ -38,7 +43,8 @@ module LogStash module Filters
38
43
  "cache_size" => cache_size,
39
44
  "tag_on_failure" => ["lookup_failed"],
40
45
  "tag_on_default_use" => ["default_used_instead"],
41
- "default_hash" => {"name" => "unknown", "location" => "unknown"}
46
+ "default_hash" => {"name" => "unknown", "location" => "unknown"},
47
+ "sequel_opts" => {"pool_timeout" => 600}
42
48
  }
43
49
  end
44
50
  let(:ipaddr) { "10.#{idx}.1.1" }
@@ -67,6 +73,32 @@ module LogStash module Filters
67
73
  end
68
74
  end
69
75
 
76
+ describe "In Prepared Statement mode, found record - uses row" do
77
+ let(:idx) { 200 }
78
+ let(:statement) { "SELECT name, location FROM reference_table WHERE ip = ?" }
79
+ let(:settings) do
80
+ {
81
+ "statement" => statement,
82
+ "use_prepared_statements" => true,
83
+ "prepared_statement_name" => "lookup_ip",
84
+ "prepared_statement_bind_values" => ["[ip]"],
85
+ "target" => "server",
86
+ "use_cache" => use_cache,
87
+ "cache_expiration" => cache_expiration,
88
+ "cache_size" => cache_size,
89
+ "tag_on_failure" => ["lookup_failed"],
90
+ "tag_on_default_use" => ["default_used_instead"],
91
+ "default_hash" => {"name" => "unknown", "location" => "unknown"},
92
+ "sequel_opts" => {"pool_timeout" => 600}
93
+ }
94
+ end
95
+ it "fills in the target" do
96
+ plugin.filter(event)
97
+ expect(event.get("server")).to eq([{"name" => "ldn-server-#{idx}", "location" => "LDN-#{idx}-2-3"}])
98
+ expect((event.get("tags") || []) & ["lookup_failed", "default_used_instead"]).to be_empty
99
+ end
100
+ end
101
+
70
102
  context "when fetching from cache" do
71
103
  let(:plugin) { TestJdbcStreaming.new(mixin_settings.merge(settings)) }
72
104
  let(:events) do
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: logstash-filter-jdbc_streaming
3
3
  version: !ruby/object:Gem::Version
4
- version: 1.0.7
4
+ version: 1.0.9
5
5
  platform: ruby
6
6
  authors:
7
7
  - Elastic
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2019-05-30 00:00:00.000000000 Z
11
+ date: 2019-10-24 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  requirement: !ruby/object:Gem::Requirement
@@ -115,6 +115,10 @@ files:
115
115
  - docs/index.asciidoc
116
116
  - lib/logstash/filters/jdbc_streaming.rb
117
117
  - lib/logstash/plugin_mixins/jdbc_streaming.rb
118
+ - lib/logstash/plugin_mixins/jdbc_streaming/cache_payload.rb
119
+ - lib/logstash/plugin_mixins/jdbc_streaming/parameter_handler.rb
120
+ - lib/logstash/plugin_mixins/jdbc_streaming/statement_handler.rb
121
+ - lib/logstash/plugin_mixins/jdbc_streaming/wrapped_driver.rb
118
122
  - logstash-filter-jdbc_streaming.gemspec
119
123
  - spec/filters/jdbc_streaming_spec.rb
120
124
  - spec/integration/jdbcstreaming_spec.rb