fluent-plugin-mysql-2 0.3.7

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.
@@ -0,0 +1,126 @@
1
+ class Fluent::MysqlOutput < Fluent::BufferedOutput
2
+ Fluent::Plugin.register_output('mysql', self)
3
+
4
+ include Fluent::SetTimeKeyMixin
5
+ include Fluent::SetTagKeyMixin
6
+
7
+ config_param :host, :string
8
+ config_param :port, :integer, :default => nil
9
+ config_param :database, :string
10
+ config_param :username, :string
11
+ config_param :password, :string, :default => '', :secret => true
12
+ config_param :sslkey, :string, :default => nil
13
+ config_param :sslcert, :string, :default => nil
14
+ config_param :sslca, :string, :default => nil
15
+ config_param :sslcapath, :string, :default => nil
16
+ config_param :sslcipher, :string, :default => nil
17
+ config_param :sslverify, :bool, :default => nil
18
+
19
+ config_param :key_names, :string, :default => nil # nil allowed for json format
20
+ config_param :sql, :string, :default => nil
21
+ config_param :table, :string, :default => nil
22
+ config_param :columns, :string, :default => nil
23
+
24
+ config_param :format, :string, :default => "raw" # or json
25
+
26
+ attr_accessor :handler
27
+
28
+ def initialize
29
+ super
30
+ require 'mysql2-cs-bind'
31
+ require 'jsonpath'
32
+ end
33
+
34
+ # Define `log` method for v0.10.42 or earlier
35
+ unless method_defined?(:log)
36
+ define_method("log") { $log }
37
+ end
38
+
39
+ def configure(conf)
40
+ super
41
+
42
+ log.warn "[mysql] This plugin deprecated. You should use mysql_bulk."
43
+
44
+ # TODO tag_mapped
45
+
46
+ case @format
47
+ when 'json'
48
+ @format_proc = Proc.new{|tag, time, record| record.to_json}
49
+ when 'jsonpath'
50
+ @key_names = @key_names.split(/\s*,\s*/)
51
+ @format_proc = Proc.new do |tag, time, record|
52
+ json = record.to_json
53
+ @key_names.map do |k|
54
+ JsonPath.new(k.strip).on(json).first
55
+ end
56
+ end
57
+ else
58
+ @key_names = @key_names.split(/\s*,\s*/)
59
+ @format_proc = Proc.new{|tag, time, record| @key_names.map{|k| record[k]}}
60
+ end
61
+
62
+ if @columns.nil? and @sql.nil?
63
+ raise Fluent::ConfigError, "columns or sql MUST be specified, but missing"
64
+ end
65
+ if @columns and @sql
66
+ raise Fluent::ConfigError, "both of columns and sql are specified, but specify one of them"
67
+ end
68
+
69
+ if @sql
70
+ begin
71
+ if @format == 'json'
72
+ Mysql2::Client.pseudo_bind(@sql, [nil])
73
+ else
74
+ Mysql2::Client.pseudo_bind(@sql, @key_names.map{|n| nil})
75
+ end
76
+ rescue ArgumentError => e
77
+ raise Fluent::ConfigError, "mismatch between sql placeholders and key_names"
78
+ end
79
+ else # columns
80
+ raise Fluent::ConfigError, "table missing" unless @table
81
+ @columns = @columns.split(/\s*,\s*/)
82
+ cols = @columns.join(',')
83
+ placeholders = if @format == 'json'
84
+ '?'
85
+ else
86
+ @key_names.map{|k| '?'}.join(',')
87
+ end
88
+ @sql = "INSERT INTO #{@table} (#{cols}) VALUES (#{placeholders})"
89
+ end
90
+ end
91
+
92
+ def start
93
+ super
94
+ end
95
+
96
+ def shutdown
97
+ super
98
+ end
99
+
100
+ def format(tag, time, record)
101
+ [tag, time, @format_proc.call(tag, time, record)].to_msgpack
102
+ end
103
+
104
+ def client
105
+ Mysql2::Client.new({
106
+ :host => @host, :port => @port,
107
+ :username => @username, :password => @password,
108
+ :database => @database,
109
+ :sslkey => @sslkey,
110
+ :sslcert => @sslcert,
111
+ :sslca => @sslca,
112
+ :sslcapath => @sslcapath,
113
+ :sslcipher => @sslcipher,
114
+ :sslverify => @sslverify,
115
+ :flags => Mysql2::Client::MULTI_STATEMENTS,
116
+ })
117
+ end
118
+
119
+ def write(chunk)
120
+ handler = self.client
121
+ chunk.msgpack_each { |tag, time, data|
122
+ handler.xquery(@sql, data)
123
+ }
124
+ handler.close
125
+ end
126
+ end
@@ -0,0 +1,234 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'fluent/plugin/output'
4
+ require 'oj'
5
+
6
+ module Fluent::Plugin
7
+ class MysqlBulkOutput < Output
8
+ Fluent::Plugin.register_output('mysql_bulk', self)
9
+
10
+ helpers :compat_parameters, :inject
11
+
12
+ config_param :host, :string, default: '127.0.0.1',
13
+ desc: "Database host."
14
+ config_param :port, :integer, default: 3306,
15
+ desc: "Database port."
16
+ config_param :database, :string,
17
+ desc: "Database name."
18
+ config_param :username, :string,
19
+ desc: "Database user."
20
+ config_param :password, :string, default: '', secret: true,
21
+ desc: "Database password."
22
+ config_param :sslkey, :string, default: nil,
23
+ desc: "SSL key."
24
+ config_param :sslcert, :string, default: nil,
25
+ desc: "SSL cert."
26
+ config_param :sslca, :string, default: nil,
27
+ desc: "SSL CA."
28
+ config_param :sslcapath, :string, default: nil,
29
+ desc: "SSL CA path."
30
+ config_param :sslcipher, :string, default: nil,
31
+ desc: "SSL cipher."
32
+ config_param :sslverify, :bool, default: nil,
33
+ desc: "SSL Verify Server Certificate."
34
+
35
+ config_param :column_names, :string,
36
+ desc: "Bulk insert column."
37
+ config_param :key_names, :string, default: nil,
38
+ desc: <<-DESC
39
+ Value key names, ${time} is placeholder Time.at(time).strftime("%Y-%m-%d %H:%M:%S").
40
+ DESC
41
+ config_param :json_key_names, :string, default: nil,
42
+ desc: "Key names which store data as json"
43
+ config_param :table, :string,
44
+ desc: "Bulk insert table."
45
+
46
+ config_param :unixtimestamp_key_names, :string, default: nil,
47
+ desc: "Key names which store data as datetime from unix time stamp"
48
+
49
+ config_param :on_duplicate_key_update, :bool, default: false,
50
+ desc: "On duplicate key update enable."
51
+ config_param :on_duplicate_update_keys, :string, default: nil,
52
+ desc: "On duplicate key update column, comma separator."
53
+ config_param :on_duplicate_update_custom_values, :string, default: nil,
54
+ desc: "On_duplicate_update_custom_values, comma separator. specify the column name is insert value, custom value is use ${sql conditions}"
55
+ config_param :insert_ignore, :bool, default: false,
56
+ :desc => "Use INSERT IGNORE"
57
+ config_param :max_rows_per_insert, :integer, default: 0,
58
+ :desc => "Maximum number of rows to insert in each statement"
59
+
60
+ config_param :transaction_isolation_level, :enum, list: [:read_uncommitted, :read_committed, :repeatable_read, :serializable], default: nil,
61
+ desc: "Set transaction isolation level."
62
+
63
+ attr_accessor :handler
64
+
65
+ def initialize
66
+ super
67
+ require 'mysql2-cs-bind'
68
+ end
69
+
70
+ def configure(conf)
71
+ compat_parameters_convert(conf, :buffer, :inject)
72
+ super
73
+
74
+ if @column_names.nil?
75
+ fail Fluent::ConfigError, 'column_names MUST specified, but missing'
76
+ end
77
+
78
+ if @on_duplicate_key_update
79
+ if @on_duplicate_update_keys.nil?
80
+ fail Fluent::ConfigError, 'on_duplicate_key_update = true , on_duplicate_update_keys nil!'
81
+ end
82
+ @on_duplicate_update_keys = @on_duplicate_update_keys.split(',')
83
+
84
+ if !@on_duplicate_update_custom_values.nil?
85
+ @on_duplicate_update_custom_values = @on_duplicate_update_custom_values.split(',')
86
+ if @on_duplicate_update_custom_values.length != @on_duplicate_update_keys.length
87
+ fail Fluent::ConfigError, <<-DESC
88
+ on_duplicate_update_keys and on_duplicate_update_custom_values must be the same length
89
+ DESC
90
+ end
91
+ end
92
+
93
+ @on_duplicate_key_update_sql = ' ON DUPLICATE KEY UPDATE '
94
+ updates = []
95
+ @on_duplicate_update_keys.each_with_index do |update_column, i|
96
+ if @on_duplicate_update_custom_values.nil? || @on_duplicate_update_custom_values[i] == "#{update_column}"
97
+ updates << "#{update_column} = VALUES(#{update_column})"
98
+ else
99
+ value = @on_duplicate_update_custom_values[i].to_s.match(/\${(.*)}/)[1]
100
+ escape_value = Mysql2::Client.escape(value)
101
+ updates << "#{update_column} = #{escape_value}"
102
+ end
103
+ end
104
+ @on_duplicate_key_update_sql += updates.join(',')
105
+ end
106
+
107
+ @column_names = @column_names.split(',').collect(&:strip)
108
+ @key_names = @key_names.nil? ? @column_names : @key_names.split(',').collect(&:strip)
109
+ @values_template = "(#{ @column_names.map { |key| '?' }.join(',') })"
110
+ @insert_columns = @column_names.map{|x| "`#{x.to_s.gsub('`', '``')}`"}.join(',')
111
+ @json_key_names = @json_key_names.split(',') if @json_key_names
112
+ @unixtimestamp_key_names = @unixtimestamp_key_names.split(',') if @unixtimestamp_key_names
113
+ end
114
+
115
+ def check_table_schema(database: @database, table: @table)
116
+ _client = client(database)
117
+ result = _client.xquery("SHOW COLUMNS FROM #{table}")
118
+ max_lengths = []
119
+ @column_names.each do |column|
120
+ info = result.select { |x| x['Field'] == column }.first
121
+ r = /(char|varchar)\(([\d]+)\)/
122
+ begin
123
+ max_length = info['Type'].scan(r)[0][1].to_i
124
+ rescue
125
+ max_length = nil
126
+ end
127
+ max_lengths << max_length
128
+ end
129
+ max_lengths
130
+ ensure
131
+ if not _client.nil? then _client.close end
132
+ end
133
+
134
+ def format(tag, time, record)
135
+ record = inject_values_to_record(tag, time, record)
136
+ [tag, time, record].to_msgpack
137
+ end
138
+
139
+ def formatted_to_msgpack_binary
140
+ true
141
+ end
142
+
143
+ def multi_workers_ready?
144
+ true
145
+ end
146
+
147
+ def client(database)
148
+ Mysql2::Client.new(
149
+ host: @host,
150
+ port: @port,
151
+ username: @username,
152
+ password: @password,
153
+ database: database,
154
+ sslkey: @sslkey,
155
+ sslcert: @sslcert,
156
+ sslca: @sslca,
157
+ sslcapath: @sslcapath,
158
+ sslcipher: @sslcipher,
159
+ sslverify: @sslverify,
160
+ flags: Mysql2::Client::MULTI_STATEMENTS
161
+ )
162
+ end
163
+
164
+ def expand_placeholders(metadata)
165
+ database = extract_placeholders(@database, metadata).gsub('.', '_')
166
+ table = extract_placeholders(@table, metadata).gsub('.', '_')
167
+ return database, table
168
+ end
169
+
170
+ def write(chunk)
171
+ database, table = expand_placeholders(chunk.metadata)
172
+ max_lengths = check_table_schema(database: database, table: table)
173
+ @handler = client(database)
174
+ values = []
175
+ chunk.msgpack_each do |tag, time, data|
176
+ data = format_proc.call(tag, time, data, max_lengths)
177
+ values << Mysql2::Client.pseudo_bind(@values_template, data)
178
+ end
179
+
180
+ @handler.query("SET SESSION TRANSACTION ISOLATION LEVEL #{transaction_isolation_level}") if @transaction_isolation_level
181
+ slice_size = @max_rows_per_insert > 0 ? @max_rows_per_insert : values.length
182
+ values.each_slice(slice_size) do |slice|
183
+ sql = "INSERT #{@insert_ignore ? "IGNORE" : ""} INTO #{table} (#{@insert_columns}) VALUES #{values.join(',')}"
184
+ sql += @on_duplicate_key_update_sql if @on_duplicate_key_update
185
+
186
+ @handler.xquery(sql)
187
+ end
188
+ log.info "bulk insert values size (table: #{@table}) => #{values.size}"
189
+ @handler.close
190
+ end
191
+
192
+ private
193
+
194
+ def format_proc
195
+ proc do |tag, time, record, max_lengths|
196
+ values = []
197
+ @key_names.each_with_index do |key, i|
198
+ if key == '${time}'
199
+ value = Time.at(time).strftime('%Y-%m-%d %H:%M:%S')
200
+ else
201
+ if max_lengths[i].nil? || record[key].nil?
202
+ value = record[key]
203
+ else
204
+ value = record[key].to_s.slice(0, max_lengths[i])
205
+ end
206
+
207
+ if @json_key_names && @json_key_names.include?(key)
208
+ value = Oj.dump(value)
209
+ end
210
+
211
+ if @unixtimestamp_key_names && @unixtimestamp_key_names.include?(key)
212
+ value = Time.at(value).strftime('%Y-%m-%d %H:%M:%S')
213
+ end
214
+ end
215
+ values << value
216
+ end
217
+ values
218
+ end
219
+ end
220
+
221
+ def transaction_isolation_level
222
+ case @transaction_isolation_level
223
+ when :read_uncommitted
224
+ "READ UNCOMMITTED"
225
+ when :read_committed
226
+ "READ COMMITTED"
227
+ when :repeatable_read
228
+ "REPEATABLE READ"
229
+ when :serializable
230
+ "SERIALIZABLE"
231
+ end
232
+ end
233
+ end
234
+ end
data/test/helper.rb ADDED
@@ -0,0 +1,29 @@
1
+ require 'rubygems'
2
+ require 'bundler'
3
+ begin
4
+ Bundler.setup(:default, :development)
5
+ rescue Bundler::BundlerError => e
6
+ $stderr.puts e.message
7
+ $stderr.puts "Run `bundle install` to install missing gems"
8
+ exit e.status_code
9
+ end
10
+ require 'test/unit'
11
+
12
+ $LOAD_PATH.unshift(File.join(File.dirname(__FILE__), '..', 'lib'))
13
+ $LOAD_PATH.unshift(File.dirname(__FILE__))
14
+ require 'fluent/test'
15
+ unless ENV.has_key?('VERBOSE')
16
+ nulllogger = Object.new
17
+ nulllogger.instance_eval {|obj|
18
+ def method_missing(method, *args)
19
+ # pass
20
+ end
21
+ }
22
+ $log = nulllogger
23
+ end
24
+
25
+ require 'fluent/plugin/out_mysql'
26
+ require 'fluent/plugin/out_mysql_bulk'
27
+
28
+ class Test::Unit::TestCase
29
+ end
@@ -0,0 +1,199 @@
1
+ require 'helper'
2
+ require 'mysql2-cs-bind'
3
+
4
+ class MysqlOutputTest < Test::Unit::TestCase
5
+ def setup
6
+ Fluent::Test.setup
7
+ end
8
+
9
+ CONFIG = %[
10
+ host db.local
11
+ database testing
12
+ username testuser
13
+ sql INSERT INTO tbl SET jsondata=?
14
+ format json
15
+ ]
16
+
17
+ def create_driver(conf = CONFIG, tag='test')
18
+ d = Fluent::Test::BufferedOutputTestDriver.new(Fluent::MysqlOutput, tag).configure(conf)
19
+ d.instance.instance_eval {
20
+ def client
21
+ obj = Object.new
22
+ obj.instance_eval {
23
+ def xquery(*args); [1]; end
24
+ def close; true; end
25
+ }
26
+ obj
27
+ end
28
+ }
29
+ d
30
+ end
31
+
32
+ def test_configure
33
+ d = create_driver %[
34
+ host database.local
35
+ database foo
36
+ username bar
37
+ sql INSERT INTO baz SET jsondata=?
38
+ format json
39
+ ]
40
+ d = create_driver %[
41
+ host database.local
42
+ database foo
43
+ username bar
44
+ table baz
45
+ columns jsondata
46
+ format json
47
+ ]
48
+ d = create_driver %[
49
+ host database.local
50
+ database foo
51
+ username bar
52
+ password mogera
53
+ key_names field1,field2,field3
54
+ table baz
55
+ columns col1, col2 ,col3
56
+ ]
57
+ assert_equal ['field1', 'field2', 'field3'], d.instance.key_names
58
+ assert_equal 'INSERT INTO baz (col1,col2,col3) VALUES (?,?,?)', d.instance.sql
59
+ d = create_driver %[
60
+ host database.local
61
+ database foo
62
+ username bar
63
+ password mogera
64
+ key_names field1 ,field2, field3
65
+ table baz
66
+ columns col1, col2 ,col3
67
+ ]
68
+ assert_equal ['field1', 'field2', 'field3'], d.instance.key_names
69
+ assert_equal 'INSERT INTO baz (col1,col2,col3) VALUES (?,?,?)', d.instance.sql
70
+
71
+ assert_raise(Fluent::ConfigError) {
72
+ d = create_driver %[
73
+ host database.local
74
+ database foo
75
+ username bar
76
+ password mogera
77
+ key_names field1,field2,field3
78
+ sql INSERT INTO baz (col1,col2,col3,col4) VALUES (?,?,?,?)
79
+ ]
80
+ }
81
+
82
+
83
+ end
84
+
85
+ def test_format
86
+ d = create_driver
87
+
88
+ time = Time.parse("2011-01-02 13:14:15 UTC").to_i
89
+ d.emit({"a"=>1}, time)
90
+ d.emit({"a"=>2}, time)
91
+
92
+ #d.expect_format %[2011-01-02T13:14:15Z\ttest\t{"a":1}\n]
93
+ #d.expect_format %[2011-01-02T13:14:15Z\ttest\t{"a":2}\n]
94
+ d.expect_format ['test', time, {"a" => 1}.to_json].to_msgpack
95
+ d.expect_format ['test', time, {"a" => 2}.to_json].to_msgpack
96
+
97
+ d.run
98
+ end
99
+
100
+ def test_time_and_tag_key
101
+ d = create_driver %[
102
+ host database.local
103
+ database foo
104
+ username bar
105
+ password mogera
106
+ include_time_key yes
107
+ utc
108
+ include_tag_key yes
109
+ table baz
110
+ key_names time,tag,field1,field2,field3,field4
111
+ sql INSERT INTO baz (coltime,coltag,col1,col2,col3,col4) VALUES (?,?,?,?,?,?)
112
+ ]
113
+ assert_equal 'INSERT INTO baz (coltime,coltag,col1,col2,col3,col4) VALUES (?,?,?,?,?,?)', d.instance.sql
114
+
115
+ time = Time.parse('2012-12-17 01:23:45 UTC').to_i
116
+ record = {'field1'=>'value1','field2'=>'value2','field3'=>'value3','field4'=>'value4'}
117
+ d.emit(record, time)
118
+ d.expect_format ['test', time, ['2012-12-17T01:23:45Z','test','value1','value2','value3','value4']].to_msgpack
119
+ d.run
120
+ end
121
+
122
+ def test_time_and_tag_key_complex
123
+ d = create_driver %[
124
+ host database.local
125
+ database foo
126
+ username bar
127
+ password mogera
128
+ include_time_key yes
129
+ utc
130
+ time_format %Y%m%d-%H%M%S
131
+ time_key timekey
132
+ include_tag_key yes
133
+ tag_key tagkey
134
+ table baz
135
+ key_names timekey,tagkey,field1,field2,field3,field4
136
+ sql INSERT INTO baz (coltime,coltag,col1,col2,col3,col4) VALUES (?,?,?,?,?,?)
137
+ ]
138
+ assert_equal 'INSERT INTO baz (coltime,coltag,col1,col2,col3,col4) VALUES (?,?,?,?,?,?)', d.instance.sql
139
+
140
+ time = Time.parse('2012-12-17 09:23:45 +0900').to_i # JST(+0900)
141
+ record = {'field1'=>'value1','field2'=>'value2','field3'=>'value3','field4'=>'value4'}
142
+ d.emit(record, time)
143
+ d.expect_format ['test', time, ['20121217-002345','test','value1','value2','value3','value4']].to_msgpack
144
+ d.run
145
+ end
146
+
147
+ def test_time_and_tag_key_json
148
+ d = create_driver %[
149
+ host database.local
150
+ database foo
151
+ username bar
152
+ password mogera
153
+ include_time_key yes
154
+ utc
155
+ time_format %Y%m%d-%H%M%S
156
+ time_key timekey
157
+ include_tag_key yes
158
+ tag_key tagkey
159
+ table accesslog
160
+ columns jsondata
161
+ format json
162
+ ]
163
+ assert_equal 'INSERT INTO accesslog (jsondata) VALUES (?)', d.instance.sql
164
+
165
+ time = Time.parse('2012-12-17 09:23:45 +0900').to_i # JST(+0900)
166
+ record = {'field1'=>'value1'}
167
+ d.emit(record, time)
168
+ # Ruby 1.9.3 Hash saves its key order, so this code is OK.
169
+ d.expect_format ['test', time, record.merge({'timekey'=>'20121217-002345','tagkey'=>'test'}).to_json].to_msgpack
170
+ d.run
171
+ end
172
+
173
+ def test_jsonpath_format
174
+ d = create_driver %[
175
+ host database.local
176
+ database foo
177
+ username bar
178
+ password mogera
179
+ include_time_key yes
180
+ utc
181
+ include_tag_key yes
182
+ table baz
183
+ format jsonpath
184
+ key_names time, tag, id, data.name, tags[0]
185
+ sql INSERT INTO baz (coltime,coltag,id,name,tag1) VALUES (?,?,?,?,?)
186
+ ]
187
+ assert_equal 'INSERT INTO baz (coltime,coltag,id,name,tag1) VALUES (?,?,?,?,?)', d.instance.sql
188
+
189
+ time = Time.parse('2012-12-17 01:23:45 UTC').to_i
190
+ record = { 'id' => 15, 'data'=> {'name' => 'jsonpath' }, 'tags' => ['unit', 'simple'] }
191
+ d.emit(record, time)
192
+ d.expect_format ['test', time, ['2012-12-17T01:23:45Z','test',15,'jsonpath','unit']].to_msgpack
193
+ d.run
194
+ end
195
+
196
+ def test_write
197
+ # hmm....
198
+ end
199
+ end