fluent-plugin-sql-enchanced 0.5.2

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA1:
3
+ metadata.gz: e1b24e490528d5366af46eb41ba43c5ddccb4276
4
+ data.tar.gz: ee88b1faf613283b94fff2ab837ad78951006981
5
+ SHA512:
6
+ metadata.gz: 6ecec17c0f370c4e762be6ed3ddfe4cbd4dfadaec1716654ab8ab4cba66a765fdc0e67eed4a106e168beaec83fca6dfa5718fe553a803a285049f22fdcc82d1d
7
+ data.tar.gz: 355e7a2999d82de43d5d7f60f2e9b712f85b984ba90c6e318d749f9d73e4c024a97718bf7780930d6ef02738a8f0653330858688c881db0a4cca5d86f3097120
data/.travis.yml ADDED
@@ -0,0 +1,32 @@
1
+ sudo: false
2
+ language: ruby
3
+
4
+ addons:
5
+ postgresql: "9.4"
6
+
7
+ rvm:
8
+ - 2.0
9
+ - 2.1
10
+ - 2.2.4
11
+ - 2.3.1
12
+ - ruby-head
13
+
14
+ gemfile:
15
+ - Gemfile
16
+ - Gemfile.v0.12
17
+
18
+ before_install:
19
+ - gem install bundler
20
+
21
+ before_script:
22
+ - psql -U postgres -c "CREATE ROLE fluentd WITH LOGIN ENCRYPTED PASSWORD 'fluentd';"
23
+ - psql -U postgres -c "CREATE DATABASE fluentd_test OWNER fluentd;"
24
+
25
+ script: bundle exec rake test
26
+
27
+ matrix:
28
+ allow_failures:
29
+ - rvm: ruby-head
30
+ exclude:
31
+ - rvm: 2.0
32
+ gemfile: Gemfile
data/Gemfile ADDED
@@ -0,0 +1,2 @@
1
+ source "http://rubygems.org"
2
+ gemspec
data/Gemfile.v0.12 ADDED
@@ -0,0 +1,6 @@
1
+ source "http://rubygems.org"
2
+
3
+ gem 'json', '= 1.8.3'
4
+ gem 'fluentd', '~> 0.12.0'
5
+
6
+ gemspec
data/README.md ADDED
@@ -0,0 +1,148 @@
1
+ # SQL input plugin for [Fluentd](http://fluentd.org) event collector
2
+
3
+ ## Overview
4
+
5
+ This SQL plugin has two parts:
6
+
7
+ 1. SQL **input** plugin reads records from RDBMSes periodically. An example use case would be getting "diffs" of a table (based on the "updated_at" field).
8
+ 2. SQL **output** plugin that writes records into RDBMes. An example use case would be aggregating server/app/sensor logs into RDBMS systems.
9
+
10
+ ## Installation
11
+
12
+ $ fluent-gem install fluent-plugin-sql --no-document
13
+ $ fluent-gem install pg --no-document # for postgresql
14
+
15
+ You should install actual RDBMS driver gem together. `pg` gem for postgresql adapter or `mysql2` gem for `mysql2` adapter. Other adapters supported by [ActiveRecord](https://github.com/rails/rails/tree/master/activerecord) should work.
16
+
17
+ We recommend that mysql2 gem is higher than `0.3.12` and pg gem is higher than `0.16.0`.
18
+
19
+ ## Input: How It Works
20
+
21
+ This plugin runs following SQL periodically:
22
+
23
+ SELECT * FROM *table* WHERE *update\_column* > *last\_update\_column\_value* ORDER BY *update_column* ASC LIMIT 500
24
+
25
+ What you need to configure is *update\_column*. The column should be an incremental column (such as AUTO\_ INCREMENT primary key) so that this plugin reads newly INSERTed rows. Alternatively, you can use a column incremented every time when you update the row (such as `last_updated_at` column) so that this plugin reads the UPDATEd rows as well.
26
+ If you omit to set *update\_column* parameter, it uses primary key.
27
+
28
+ It stores last selected rows to a file (named *state\_file*) to not forget the last row when Fluentd restarts.
29
+
30
+ ## Input: Configuration
31
+
32
+ <source>
33
+ @type sql
34
+
35
+ host rdb_host
36
+ database rdb_database
37
+ adapter mysql2_or_postgresql_or_etc
38
+ username myusername
39
+ password mypassword
40
+
41
+ tag_prefix my.rdb # optional, but recommended
42
+
43
+ select_interval 60s # optional
44
+ select_limit 500 # optional
45
+
46
+ state_file /var/run/fluentd/sql_state
47
+
48
+ <table>
49
+ table table1
50
+ tag table1 # optional
51
+ update_column update_col1
52
+ time_column time_col2 # optional
53
+ </table>
54
+
55
+ <table>
56
+ table table2
57
+ tag table2 # optional
58
+ update_column updated_at
59
+ time_column updated_at # optional
60
+ </table>
61
+
62
+ # detects all tables instead of <table> sections
63
+ #all_tables
64
+ </source>
65
+
66
+ * **host** RDBMS host
67
+ * **port** RDBMS port
68
+ * **database** RDBMS database name
69
+ * **adapter** RDBMS driver name. You should install corresponding gem before start (mysql2 gem for mysql2 adapter, pg gem for postgresql adapter, etc.)
70
+ * **username** RDBMS login user name
71
+ * **password** RDBMS login password
72
+ * **tag_prefix** prefix of tags of events. actual tag will be this\_tag\_prefix.tables\_tag (optional)
73
+ * **select_interval** interval to run SQLs (optional)
74
+ * **select_limit** LIMIT of number of rows for each SQL (optional)
75
+ * **state_file** path to a file to store last rows
76
+ * **all_tables** reads all tables instead of configuring each tables in \<table\> sections
77
+
78
+ \<table\> sections:
79
+
80
+ * **tag** tag name of events (optional; default value is table name)
81
+ * **table** RDBM table name
82
+ * **update_column**: see above description
83
+ * **time_column** (optional): if this option is set, this plugin uses this column's value as the the event's time. Otherwise it uses current time.
84
+ * **primary_key** (optional): if you want to get data from the table which doesn't have primary key like PostgreSQL's View, set this parameter.
85
+
86
+ ## Input: Limitation
87
+
88
+ You should make sure target tables have index (and/or partitions) on the *update\_column*. Otherwise SELECT causes full table scan and serious performance problem.
89
+
90
+ You can't replicate DELETEd rows.
91
+
92
+ ## Output: How It Works
93
+
94
+ This plugin takes advantage of ActiveRecord underneath. For `host`, `port`, `database`, `adapter`, `username`, `password`, `socket` parameters, you can think of ActiveRecord's equivalent parameters.
95
+
96
+ ## Output: Configuration
97
+
98
+ <match my.rdb.*>
99
+ @type sql
100
+ host rdb_host
101
+ port 3306
102
+ database rdb_database
103
+ adapter mysql2_or_postgresql_or_etc
104
+ username myusername
105
+ password mypassword
106
+ socket path_to_socket
107
+ remove_tag_prefix my.rdb # optional, dual of tag_prefix in in_sql
108
+
109
+ <table>
110
+ table table1
111
+ column_mapping 'timestamp:created_at,fluentdata1:dbcol1,fluentdata2:dbcol2,fluentdata3:dbcol3'
112
+ # This is the default table because it has no "pattern" argument in <table>
113
+ # The logic is such that if all non-default <table> blocks
114
+ # do not match, the default one is chosen.
115
+ # The default table is required.
116
+ </table>
117
+
118
+ <table hello.*> # You can pass the same pattern you use in match statements.
119
+ table table2
120
+ # This is the non-default table. It is chosen if the tag matches the pattern
121
+ # AFTER remove_tag_prefix is applied to the incoming event. For example, if
122
+ # the message comes in with the tag my.rdb.hello.world, "remove_tag_prefix my.rdb"
123
+ # makes it "hello.world", which gets matched here because of "pattern hello.*".
124
+ </table>
125
+
126
+ <table hello.world>
127
+ table table3
128
+ # This is the second non-default table. You can have as many non-default tables
129
+ # as you wish. One caveat: non-default tables are matched top-to-bottom and
130
+ # the events go into the first table it matches to. Hence, this particular table
131
+ # never gets any data, since the above "hello.*" subsumes "hello.world".
132
+ </table>
133
+ </match>
134
+
135
+ * **host** RDBMS host
136
+ * **port** RDBMS port
137
+ * **database** RDBMS database name
138
+ * **adapter** RDBMS driver name. You should install corresponding gem before start (mysql2 gem for mysql2 adapter, pg gem for postgresql adapter, etc.)
139
+ * **username** RDBMS login user name
140
+ * **password** RDBMS login password
141
+ * **socket** RDBMS socket path
142
+ * **remove_tag_prefix** remove the given prefix from the events. See "tag_prefix" in "Input: Configuration". (optional)
143
+
144
+ \<table\> sections:
145
+
146
+ * **table** RDBM table name
147
+ * **column_mapping**: [Required] Record to table schema mapping. The format is consists of `from:to` or `key` values are separated by `,`. For example, if set 'item_id:id,item_text:data,updated_at' to **column_mapping**, `item_id` field of record is stored into `id` column and `updated_at` field of record is stored into `updated_at` column.
148
+ * **<table pattern>**: the pattern to which the incoming event's tag (after it goes through `remove_tag_prefix`, if given). The patterns should follow the same syntax as [that of <match>](http://docs.fluentd.org/articles/config-file#match-pattern-how-you-control-the-event-flow-inside-fluentd). **Exactly one <table> element must NOT have this parameter so that it becomes the default table to store data**.
data/Rakefile ADDED
@@ -0,0 +1,14 @@
1
+
2
+ require 'bundler'
3
+ Bundler::GemHelper.install_tasks
4
+
5
+ require 'rake/testtask'
6
+
7
+ Rake::TestTask.new(:test) do |test|
8
+ test.libs << 'lib' << 'test'
9
+ test.test_files = FileList['test/*/*.rb']
10
+ test.verbose = true
11
+ end
12
+
13
+ task :default => [:build]
14
+
data/VERSION ADDED
@@ -0,0 +1 @@
1
+ 0.5.2
@@ -0,0 +1,28 @@
1
+ # encoding: utf-8
2
+ $:.push File.expand_path('../lib', __FILE__)
3
+
4
+ Gem::Specification.new do |gem|
5
+ gem.name = "fluent-plugin-sql-enchanced"
6
+ gem.description = "SQL input/output plugin for Fluentd event collector"
7
+ gem.homepage = "https://github.com/zhron4x/fluent-plugin-sql-enchanced"
8
+ gem.summary = gem.description
9
+ gem.version = File.read("VERSION").strip
10
+ gem.authors = ["Sadayuki Furuhashi"]
11
+ gem.email = "frsyuki@gmail.com"
12
+ gem.has_rdoc = false
13
+ #gem.platform = Gem::Platform::RUBY
14
+ gem.files = `git ls-files`.split("\n")
15
+ gem.test_files = `git ls-files -- {test,spec,features}/*`.split("\n")
16
+ gem.executables = `git ls-files -- bin/*`.split("\n").map{ |f| File.basename(f) }
17
+ gem.require_paths = ['lib']
18
+ gem.license = "Apache-2.0"
19
+
20
+ gem.add_dependency "fluentd", [">= 0.12.17", "< 2"]
21
+ gem.add_dependency 'activerecord', "~> 4.2"
22
+ gem.add_dependency 'activerecord-import', "~> 0.7"
23
+ gem.add_development_dependency "rake", ">= 0.9.2"
24
+ gem.add_development_dependency "test-unit", "~> 3.1.0"
25
+ gem.add_development_dependency "test-unit-rr"
26
+ gem.add_development_dependency "test-unit-notify"
27
+ gem.add_development_dependency "pg"
28
+ end
@@ -0,0 +1,194 @@
1
+ class Fluentd
2
+ module Setting
3
+ class InSql
4
+ include ActiveModel::Model
5
+ include Common
6
+
7
+ KEYS = [
8
+ :host,
9
+ :port,
10
+ :database,
11
+ :adapter,
12
+ :username,
13
+ :password,
14
+ :tag_prefix,
15
+ :select_interval,
16
+ :select_limit,
17
+ :state_file,
18
+ :table,
19
+ :all_tables
20
+ ].freeze
21
+
22
+ attr_accessor(*KEYS)
23
+ attr_accessor(:fields_descriptions)
24
+
25
+ validates :host, presence: true
26
+ validates :database, presence: true
27
+ validates :adapter, presence: true
28
+ validates :username, presence: true
29
+ validates :password, presence: true
30
+ validates :state_file, presence: true
31
+
32
+ def self.initial_params
33
+ {
34
+ host: 'localhost',
35
+ database: 'rdb_database',
36
+ adapter: 'mysql2',
37
+ username: 'myusername',
38
+ password: 'mypassword',
39
+ tag_prefix: 'my.rdb',
40
+ select_interval: '60s',
41
+ select_limit: '500',
42
+ state_file: "/tmp/data_enchilada-sql-#{Fluentd.instance.id}-#{Time.now.to_i}.pos",
43
+ table: [
44
+ {
45
+ table: 'rdb_table',
46
+ tag: 'rdb_table_tag',
47
+ update_column: 'updated_at',
48
+ time_column: 'updated_at',
49
+ primary_key: ''
50
+ }
51
+ ]
52
+ }
53
+ end
54
+
55
+ def fields_descriptions
56
+ {
57
+ host: '* RDBMS host (required)',
58
+ port: 'RDBMS port (optional)',
59
+ database: '* RDBMS database name (required)',
60
+ adapter: '* RDBMS driver name. You should install corresponding gem before start (mysql2 gem for mysql2 adapter, pg gem for postgresql adapter, etc. (required)',
61
+ username: '* RDBMS login user name (required)',
62
+ password: '* RDBMS login password (required)',
63
+ tag_prefix: 'prefix of tags of events. actual tag will be this_tag_prefix.tables_tag (optional, but recommended)',
64
+ select_interval: 'interval to run SQLs (optional)',
65
+ select_limit: 'LIMIT of number of rows for each SQL (optional)',
66
+ state_file: '* path to a file to store last rows (required)',
67
+ all_tables: 'reads all tables instead of configuring each tables in &lt;table&gt; sections (optional)',
68
+ table: {
69
+ tag: 'tag name of events (optional; default value is table name)',
70
+ table: '* RDBM table name',
71
+ update_column: '* see above description',
72
+ time_column: "(optional): if this option is set, this plugin uses this column's value as the the event's time. Otherwise it uses current time.",
73
+ primary_key: "(optional): if you want to get data from the table which doesn't have primary key like PostgreSQL's View, set this parameter."
74
+ }
75
+ }
76
+ end
77
+
78
+ def common_options
79
+ [
80
+ :host,
81
+ :port,
82
+ :database,
83
+ :adapter,
84
+ :username,
85
+ :password,
86
+ :tag_prefix
87
+ ]
88
+ end
89
+
90
+ def advanced_options
91
+ [
92
+ :select_interval,
93
+ :select_limit,
94
+ :state_file
95
+ ]
96
+ end
97
+
98
+ def table=(value)
99
+ @tables = value.map do |t|
100
+ t.map{|field, value| ([field.to_sym, value] if table_fields.include?(field.to_sym))}.to_h
101
+ end
102
+ end
103
+
104
+ def table
105
+ @tables
106
+ end
107
+
108
+ def table_fields
109
+ [
110
+ :table,
111
+ :tag,
112
+ :update_column,
113
+ :time_column,
114
+ :primary_key
115
+ ]
116
+ end
117
+
118
+ def to_config
119
+ indent = " "
120
+ config = "<source>\n"
121
+ config << "#{indent}type #{plugin_type_name}\n"
122
+ self.class.const_get(:KEYS).each do |key|
123
+ next if key == :table
124
+ next if key == :all_tables
125
+ config << indent
126
+ config << conf(key)
127
+ config << "\n"
128
+ end
129
+ tables = send(:table).reject{|t| t.values.join('') == ''} rescue []
130
+ if tables.present? && all_tables != '1'
131
+ tables.each do |tab|
132
+ config << "\n"
133
+ config << indent
134
+ config << "<table>\n"
135
+ tab.each do |key, value|
136
+ config << indent
137
+ config << indent
138
+ config << "#{key} #{value}"
139
+ config << "\n"
140
+ end
141
+ config << indent
142
+ config << "</table>\n"
143
+ end
144
+ else
145
+ config << indent
146
+ config << 'all_tables'
147
+ config << "\n"
148
+ end
149
+
150
+
151
+ config << "</source>\n"
152
+ config.gsub(/^[ ]*\n/m, "")
153
+ end
154
+
155
+ def self.create_from_config config
156
+ @setting = {}
157
+ @tables = []
158
+ table = {}
159
+ to_table = false
160
+ str_params = config.split("\r\n").map{|str| str.squish}
161
+ str_params.each do |str|
162
+ case str
163
+ when '<source>', '</source>'
164
+ next
165
+ when '<table>'
166
+ to_table = true
167
+ when '</table>'
168
+ to_table = false
169
+ @tables << table
170
+ table = {}
171
+ when 'all_tables'
172
+ @setting[:all_tables] = true
173
+ else
174
+ param = str.split(' ')
175
+ if to_table
176
+ table[param.first] = param.second
177
+ else
178
+ @setting[param.first] = param.second unless param.first == 'type'
179
+ end
180
+ end
181
+ if @tables.present?
182
+ @setting[:table] = @tables
183
+ end
184
+ end
185
+ self.new @setting
186
+ end
187
+
188
+ def plugin_name
189
+ "sql"
190
+ end
191
+ end
192
+ end
193
+ end
194
+
@@ -0,0 +1,261 @@
1
+ require "fluent/output"
2
+
3
+ module Fluent
4
+ class SQLOutput < BufferedOutput
5
+ Plugin.register_output('sql', self)
6
+
7
+ include SetTimeKeyMixin
8
+ include SetTagKeyMixin
9
+
10
+ # For fluentd v0.12.16 or earlier
11
+ class << self
12
+ unless method_defined?(:desc)
13
+ def desc(description)
14
+ end
15
+ end
16
+ end
17
+
18
+ desc 'RDBMS host'
19
+ config_param :host, :string
20
+ desc 'RDBMS port'
21
+ config_param :port, :integer, :default => nil
22
+ desc 'RDBMS driver name.'
23
+ config_param :adapter, :string
24
+ desc 'RDBMS login user name'
25
+ config_param :username, :string, :default => nil
26
+ desc 'RDBMS login password'
27
+ config_param :password, :string, :default => nil, :secret => true
28
+ desc 'RDBMS database name'
29
+ config_param :database, :string
30
+ desc 'RDBMS socket path'
31
+ config_param :socket, :string, :default => nil
32
+ desc 'remove the given prefix from the events'
33
+ config_param :remove_tag_prefix, :string, :default => nil
34
+ desc 'enable fallback'
35
+ config_param :enable_fallback, :bool, :default => true
36
+
37
+ attr_accessor :tables
38
+
39
+ unless method_defined?(:log)
40
+ define_method(:log) { $log }
41
+ end
42
+
43
+ # TODO: Merge SQLInput's TableElement
44
+ class TableElement
45
+ include Configurable
46
+
47
+ config_param :table, :string
48
+ config_param :column_mapping, :string
49
+ config_param :num_retries, :integer, :default => 5
50
+
51
+ attr_reader :model
52
+ attr_reader :pattern
53
+
54
+ def initialize(pattern, log, enable_fallback)
55
+ super()
56
+ @pattern = MatchPattern.create(pattern)
57
+ @log = log
58
+ @enable_fallback = enable_fallback
59
+ end
60
+
61
+ def configure(conf)
62
+ super
63
+
64
+ @mapping = parse_column_mapping(@column_mapping)
65
+ @format_proc = Proc.new { |record|
66
+ new_record = {}
67
+ @mapping.each { |k, c|
68
+ new_record[c] = record[k]
69
+ }
70
+ new_record
71
+ }
72
+ end
73
+
74
+ def init(base_model)
75
+ # See SQLInput for more details of following code
76
+ table_name = @table
77
+ @model = Class.new(base_model) do
78
+ self.table_name = table_name
79
+ self.inheritance_column = '_never_use_output_'
80
+ end
81
+
82
+ class_name = table_name.singularize.camelize
83
+ base_model.const_set(class_name, @model)
84
+ model_name = ActiveModel::Name.new(@model, nil, class_name)
85
+ @model.define_singleton_method(:model_name) { model_name }
86
+
87
+ # TODO: check column_names and table schema
88
+ # @model.column_names
89
+ end
90
+
91
+ def import(chunk)
92
+ records = []
93
+ chunk.msgpack_each { |tag, time, data|
94
+ begin
95
+ # format process should be moved to emit / format after supports error stream.
96
+ records << @model.new(@format_proc.call(data))
97
+ rescue => e
98
+ args = {:error => e.message, :error_class => e.class, :table => @table, :record => Yajl.dump(data)}
99
+ @log.warn "Failed to create the model. Ignore a record:", args
100
+ end
101
+ }
102
+ begin
103
+ @model.import(records)
104
+ rescue ActiveRecord::StatementInvalid, ActiveRecord::Import::MissingColumnError => e
105
+ if @enable_fallback
106
+ # ignore other exceptions to use Fluentd retry mechanizm
107
+ @log.warn "Got deterministic error. Fallback to one-by-one import", :error => e.message, :error_class => e.class
108
+ one_by_one_import(records)
109
+ else
110
+ $log.warn "Got deterministic error. Fallback is disabled", :error => e.message, :error_class => e.class
111
+ raise e
112
+ end
113
+ end
114
+ end
115
+
116
+ def one_by_one_import(records)
117
+ records.each { |record|
118
+ retries = 0
119
+ begin
120
+ @model.import([record])
121
+ rescue ActiveRecord::StatementInvalid, ActiveRecord::Import::MissingColumnError => e
122
+ @log.error "Got deterministic error again. Dump a record", :error => e.message, :error_class => e.class, :record => record
123
+ rescue => e
124
+ retries += 1
125
+ if retries > @num_retries
126
+ @log.error "Can't recover undeterministic error. Dump a record", :error => e.message, :error_class => e.class, :record => record
127
+ next
128
+ end
129
+
130
+ @log.warn "Failed to import a record: retry number = #{retries}", :error => e.message, :error_class => e.class
131
+ sleep 0.5
132
+ retry
133
+ end
134
+ }
135
+ end
136
+
137
+ private
138
+
139
+ def parse_column_mapping(column_mapping_conf)
140
+ mapping = {}
141
+ column_mapping_conf.split(',').each { |column_map|
142
+ key, column = column_map.strip.split(':', 2)
143
+ column = key if column.nil?
144
+ mapping[key] = column
145
+ }
146
+ mapping
147
+ end
148
+ end
149
+
150
+ def initialize
151
+ super
152
+ require 'active_record'
153
+ require 'activerecord-import'
154
+ end
155
+
156
+ def configure(conf)
157
+ super
158
+
159
+ if remove_tag_prefix = conf['remove_tag_prefix']
160
+ @remove_tag_prefix = Regexp.new('^' + Regexp.escape(remove_tag_prefix))
161
+ end
162
+
163
+ @tables = []
164
+ @default_table = nil
165
+ conf.elements.select { |e|
166
+ e.name == 'table'
167
+ }.each { |e|
168
+ te = TableElement.new(e.arg, log, @enable_fallback)
169
+ te.configure(e)
170
+ if e.arg.empty?
171
+ $log.warn "Detect duplicate default table definition" if @default_table
172
+ @default_table = te
173
+ else
174
+ @tables << te
175
+ end
176
+ }
177
+ @only_default = @tables.empty?
178
+
179
+ if @default_table.nil?
180
+ raise ConfigError, "There is no default table. <table> is required in sql output"
181
+ end
182
+ end
183
+
184
+ def start
185
+ super
186
+
187
+ config = {
188
+ :adapter => @adapter,
189
+ :host => @host,
190
+ :port => @port,
191
+ :database => @database,
192
+ :username => @username,
193
+ :password => @password,
194
+ :socket => @socket,
195
+ }
196
+
197
+ @base_model = Class.new(ActiveRecord::Base) do
198
+ self.abstract_class = true
199
+ end
200
+
201
+ SQLOutput.const_set("BaseModel_#{rand(1 << 31)}", @base_model)
202
+ ActiveRecord::Base.establish_connection(config)
203
+
204
+ # ignore tables if TableElement#init failed
205
+ @tables.reject! do |te|
206
+ init_table(te, @base_model)
207
+ end
208
+ init_table(@default_table, @base_model)
209
+ end
210
+
211
+ def shutdown
212
+ super
213
+ end
214
+
215
+ def emit(tag, es, chain)
216
+ if @only_default
217
+ super(tag, es, chain)
218
+ else
219
+ super(tag, es, chain, format_tag(tag))
220
+ end
221
+ end
222
+
223
+ def format(tag, time, record)
224
+ [tag, time, record].to_msgpack
225
+ end
226
+
227
+ def write(chunk)
228
+ ActiveRecord::Base.connection_pool.with_connection do
229
+
230
+ @tables.each { |table|
231
+ if table.pattern.match(chunk.key)
232
+ return table.import(chunk)
233
+ end
234
+ }
235
+ @default_table.import(chunk)
236
+ end
237
+ end
238
+
239
+ private
240
+
241
+ def init_table(te, base_model)
242
+ begin
243
+ te.init(base_model)
244
+ log.info "Selecting '#{te.table}' table"
245
+ false
246
+ rescue => e
247
+ log.warn "Can't handle '#{te.table}' table. Ignoring.", :error => e.message, :error_class => e.class
248
+ log.warn_backtrace e.backtrace
249
+ true
250
+ end
251
+ end
252
+
253
+ def format_tag(tag)
254
+ if @remove_tag_prefix
255
+ tag.gsub(@remove_tag_prefix, '')
256
+ else
257
+ tag
258
+ end
259
+ end
260
+ end
261
+ end
@@ -0,0 +1,24 @@
1
+ require "active_record"
2
+ ActiveRecord::Base.establish_connection(host: "localhost",
3
+ port: 5432,
4
+ username: "fluentd",
5
+ password: "fluentd",
6
+ adapter: "postgresql",
7
+ database: "fluentd_test")
8
+ ActiveRecord::Schema.define(version: 20160225030107) do
9
+ create_table "logs", force: :cascade do |t|
10
+ t.string "host"
11
+ t.string "ident"
12
+ t.string "pid"
13
+ t.text "message"
14
+ t.datetime "created_at", null: false
15
+ t.datetime "updated_at", null: false
16
+ end
17
+
18
+ create_table "messages", force: :cascade do |t|
19
+ t.string "message"
20
+ t.datetime "created_at", null: false
21
+ t.datetime "updated_at", null: false
22
+ end
23
+ end
24
+
data/test/helper.rb ADDED
@@ -0,0 +1,8 @@
1
+ require "test/unit"
2
+ require "test/unit/rr"
3
+ require "test/unit/notify"
4
+ require "fluent/test"
5
+ require "fluent/plugin/out_sql"
6
+ require "fluent/plugin/in_sql"
7
+
8
+ load "fixtures/schema.rb"
@@ -0,0 +1,86 @@
1
+ require "helper"
2
+
3
+ class SqlInputTest < Test::Unit::TestCase
4
+ def setup
5
+ Fluent::Test.setup
6
+ end
7
+
8
+ def teardown
9
+ end
10
+
11
+ CONFIG = %[
12
+ adapter postgresql
13
+ host localhost
14
+ port 5432
15
+ database fluentd_test
16
+
17
+ username fluentd
18
+ password fluentd
19
+
20
+ tag_prefix db
21
+
22
+ <table>
23
+ table messages
24
+ tag logs
25
+ update_column updated_at
26
+ time_column updated_at
27
+ </table>
28
+ ]
29
+
30
+ def create_driver(conf = CONFIG)
31
+ Fluent::Test::InputTestDriver.new(Fluent::SQLInput).configure(conf)
32
+ end
33
+
34
+ def test_configure
35
+ d = create_driver
36
+ expected = {
37
+ host: "localhost",
38
+ port: 5432,
39
+ adapter: "postgresql",
40
+ database: "fluentd_test",
41
+ username: "fluentd",
42
+ password: "fluentd",
43
+ tag_prefix: "db"
44
+ }
45
+ actual = {
46
+ host: d.instance.host,
47
+ port: d.instance.port,
48
+ adapter: d.instance.adapter,
49
+ database: d.instance.database,
50
+ username: d.instance.username,
51
+ password: d.instance.password,
52
+ tag_prefix: d.instance.tag_prefix
53
+ }
54
+ assert_equal(expected, actual)
55
+ tables = d.instance.instance_variable_get(:@tables)
56
+ assert_equal(1, tables.size)
57
+ messages = tables.first
58
+ assert_equal("messages", messages.table)
59
+ assert_equal("logs", messages.tag)
60
+ end
61
+
62
+ def test_message
63
+ d = create_driver(CONFIG + "select_interval 1")
64
+ Message.create!(message: "message 1")
65
+ Message.create!(message: "message 2")
66
+ Message.create!(message: "message 3")
67
+
68
+ d.run
69
+
70
+ assert_equal("db.logs", d.emits[0][0])
71
+ expected = [
72
+ [d.emits[0][1], "message 1"],
73
+ [d.emits[1][1], "message 2"],
74
+ [d.emits[2][1], "message 3"],
75
+ ]
76
+ actual = [
77
+ [Time.parse(d.emits[0][2]["updated_at"]).to_i, d.emits[0][2]["message"]],
78
+ [Time.parse(d.emits[1][2]["updated_at"]).to_i, d.emits[1][2]["message"]],
79
+ [Time.parse(d.emits[2][2]["updated_at"]).to_i, d.emits[2][2]["message"]],
80
+ ]
81
+ assert_equal(expected, actual)
82
+ end
83
+
84
+ class Message < ActiveRecord::Base
85
+ end
86
+ end
@@ -0,0 +1,114 @@
1
+ require "helper"
2
+
3
+ class SqlOutputTest < Test::Unit::TestCase
4
+ def setup
5
+ Fluent::Test.setup
6
+ end
7
+
8
+ def teardown
9
+ end
10
+
11
+ CONFIG = %[
12
+ host localhost
13
+ port 5432
14
+ adapter postgresql
15
+
16
+ database fluentd_test
17
+ username fluentd
18
+ password fluentd
19
+
20
+ remove_tag_prefix db
21
+
22
+ <table>
23
+ table logs
24
+ column_mapping timestamp:created_at,host:host,ident:ident,pid:pid,message:message
25
+ </table>
26
+ ]
27
+
28
+ def create_driver(conf = CONFIG)
29
+ Fluent::Test::BufferedOutputTestDriver.new(Fluent::SQLOutput).configure(conf)
30
+ end
31
+
32
+ def test_configure
33
+ d = create_driver
34
+ expected = {
35
+ host: "localhost",
36
+ port: 5432,
37
+ adapter: "postgresql",
38
+ database: "fluentd_test",
39
+ username: "fluentd",
40
+ password: "fluentd",
41
+ remove_tag_suffix: /^db/,
42
+ enable_fallback: true
43
+ }
44
+ actual = {
45
+ host: d.instance.host,
46
+ port: d.instance.port,
47
+ adapter: d.instance.adapter,
48
+ database: d.instance.database,
49
+ username: d.instance.username,
50
+ password: d.instance.password,
51
+ remove_tag_suffix: d.instance.remove_tag_prefix,
52
+ enable_fallback: d.instance.enable_fallback
53
+ }
54
+ assert_equal(expected, actual)
55
+ assert_empty(d.instance.tables)
56
+ default_table = d.instance.instance_variable_get(:@default_table)
57
+ assert_equal("logs", default_table.table)
58
+ end
59
+
60
+ def test_emit
61
+ d = create_driver
62
+ time = Time.parse("2011-01-02 13:14:15 UTC").to_i
63
+
64
+ d.emit({"message" => "message1"}, time)
65
+ d.emit({"message" => "message2"}, time)
66
+
67
+ d.run
68
+
69
+ default_table = d.instance.instance_variable_get(:@default_table)
70
+ model = default_table.instance_variable_get(:@model)
71
+ assert_equal(2, model.all.count)
72
+ messages = model.pluck(:message).sort
73
+ assert_equal(["message1", "message2"], messages)
74
+ end
75
+
76
+ class Fallback < self
77
+ def test_simple
78
+ d = create_driver
79
+ time = Time.parse("2011-01-02 13:14:15 UTC").to_i
80
+
81
+ d.emit({"message" => "message1"}, time)
82
+ d.emit({"message" => "message2"}, time)
83
+
84
+ d.run do
85
+ default_table = d.instance.instance_variable_get(:@default_table)
86
+ model = default_table.instance_variable_get(:@model)
87
+ mock(model).import(anything).at_least(1) do
88
+ raise ActiveRecord::Import::MissingColumnError.new("dummy_table", "dummy_column")
89
+ end
90
+ mock(default_table).one_by_one_import(anything)
91
+ end
92
+ end
93
+
94
+ def test_limit
95
+ d = create_driver
96
+ time = Time.parse("2011-01-02 13:14:15 UTC").to_i
97
+
98
+ d.emit({"message" => "message1"}, time)
99
+ d.emit({"message" => "message2"}, time)
100
+
101
+ d.run do
102
+ default_table = d.instance.instance_variable_get(:@default_table)
103
+ model = default_table.instance_variable_get(:@model)
104
+ mock(model).import([anything, anything]).once do
105
+ raise ActiveRecord::Import::MissingColumnError.new("dummy_table", "dummy_column")
106
+ end
107
+ mock(model).import([anything]).times(12) do
108
+ raise StandardError
109
+ end
110
+ assert_equal(5, default_table.instance_variable_get(:@num_retries))
111
+ end
112
+ end
113
+ end
114
+ end
metadata ADDED
@@ -0,0 +1,174 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: fluent-plugin-sql-enchanced
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.5.2
5
+ platform: ruby
6
+ authors:
7
+ - Sadayuki Furuhashi
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain: []
11
+ date: 2017-03-31 00:00:00.000000000 Z
12
+ dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: fluentd
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - ">="
18
+ - !ruby/object:Gem::Version
19
+ version: 0.12.17
20
+ - - "<"
21
+ - !ruby/object:Gem::Version
22
+ version: '2'
23
+ type: :runtime
24
+ prerelease: false
25
+ version_requirements: !ruby/object:Gem::Requirement
26
+ requirements:
27
+ - - ">="
28
+ - !ruby/object:Gem::Version
29
+ version: 0.12.17
30
+ - - "<"
31
+ - !ruby/object:Gem::Version
32
+ version: '2'
33
+ - !ruby/object:Gem::Dependency
34
+ name: activerecord
35
+ requirement: !ruby/object:Gem::Requirement
36
+ requirements:
37
+ - - "~>"
38
+ - !ruby/object:Gem::Version
39
+ version: '4.2'
40
+ type: :runtime
41
+ prerelease: false
42
+ version_requirements: !ruby/object:Gem::Requirement
43
+ requirements:
44
+ - - "~>"
45
+ - !ruby/object:Gem::Version
46
+ version: '4.2'
47
+ - !ruby/object:Gem::Dependency
48
+ name: activerecord-import
49
+ requirement: !ruby/object:Gem::Requirement
50
+ requirements:
51
+ - - "~>"
52
+ - !ruby/object:Gem::Version
53
+ version: '0.7'
54
+ type: :runtime
55
+ prerelease: false
56
+ version_requirements: !ruby/object:Gem::Requirement
57
+ requirements:
58
+ - - "~>"
59
+ - !ruby/object:Gem::Version
60
+ version: '0.7'
61
+ - !ruby/object:Gem::Dependency
62
+ name: rake
63
+ requirement: !ruby/object:Gem::Requirement
64
+ requirements:
65
+ - - ">="
66
+ - !ruby/object:Gem::Version
67
+ version: 0.9.2
68
+ type: :development
69
+ prerelease: false
70
+ version_requirements: !ruby/object:Gem::Requirement
71
+ requirements:
72
+ - - ">="
73
+ - !ruby/object:Gem::Version
74
+ version: 0.9.2
75
+ - !ruby/object:Gem::Dependency
76
+ name: test-unit
77
+ requirement: !ruby/object:Gem::Requirement
78
+ requirements:
79
+ - - "~>"
80
+ - !ruby/object:Gem::Version
81
+ version: 3.1.0
82
+ type: :development
83
+ prerelease: false
84
+ version_requirements: !ruby/object:Gem::Requirement
85
+ requirements:
86
+ - - "~>"
87
+ - !ruby/object:Gem::Version
88
+ version: 3.1.0
89
+ - !ruby/object:Gem::Dependency
90
+ name: test-unit-rr
91
+ requirement: !ruby/object:Gem::Requirement
92
+ requirements:
93
+ - - ">="
94
+ - !ruby/object:Gem::Version
95
+ version: '0'
96
+ type: :development
97
+ prerelease: false
98
+ version_requirements: !ruby/object:Gem::Requirement
99
+ requirements:
100
+ - - ">="
101
+ - !ruby/object:Gem::Version
102
+ version: '0'
103
+ - !ruby/object:Gem::Dependency
104
+ name: test-unit-notify
105
+ requirement: !ruby/object:Gem::Requirement
106
+ requirements:
107
+ - - ">="
108
+ - !ruby/object:Gem::Version
109
+ version: '0'
110
+ type: :development
111
+ prerelease: false
112
+ version_requirements: !ruby/object:Gem::Requirement
113
+ requirements:
114
+ - - ">="
115
+ - !ruby/object:Gem::Version
116
+ version: '0'
117
+ - !ruby/object:Gem::Dependency
118
+ name: pg
119
+ requirement: !ruby/object:Gem::Requirement
120
+ requirements:
121
+ - - ">="
122
+ - !ruby/object:Gem::Version
123
+ version: '0'
124
+ type: :development
125
+ prerelease: false
126
+ version_requirements: !ruby/object:Gem::Requirement
127
+ requirements:
128
+ - - ">="
129
+ - !ruby/object:Gem::Version
130
+ version: '0'
131
+ description: SQL input/output plugin for Fluentd event collector
132
+ email: frsyuki@gmail.com
133
+ executables: []
134
+ extensions: []
135
+ extra_rdoc_files: []
136
+ files:
137
+ - ".travis.yml"
138
+ - Gemfile
139
+ - Gemfile.v0.12
140
+ - README.md
141
+ - Rakefile
142
+ - VERSION
143
+ - fluent-plugin-sql.gemspec
144
+ - lib/fluent/plugin/in_sql.rb
145
+ - lib/fluent/plugin/out_sql.rb
146
+ - test/fixtures/schema.rb
147
+ - test/helper.rb
148
+ - test/plugin/test_in_sql.rb
149
+ - test/plugin/test_out_sql.rb
150
+ homepage: https://github.com/zhron4x/fluent-plugin-sql-enchanced
151
+ licenses:
152
+ - Apache-2.0
153
+ metadata: {}
154
+ post_install_message:
155
+ rdoc_options: []
156
+ require_paths:
157
+ - lib
158
+ required_ruby_version: !ruby/object:Gem::Requirement
159
+ requirements:
160
+ - - ">="
161
+ - !ruby/object:Gem::Version
162
+ version: '0'
163
+ required_rubygems_version: !ruby/object:Gem::Requirement
164
+ requirements:
165
+ - - ">="
166
+ - !ruby/object:Gem::Version
167
+ version: '0'
168
+ requirements: []
169
+ rubyforge_project:
170
+ rubygems_version: 2.6.8
171
+ signing_key:
172
+ specification_version: 4
173
+ summary: SQL input/output plugin for Fluentd event collector
174
+ test_files: []