fluent-plugin-sql-enchanced 0.5.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
checksums.yaml 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: []