fluent-plugin-sql 0.2.1

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.
data/Gemfile ADDED
@@ -0,0 +1,2 @@
1
+ source "http://rubygems.org"
2
+ gemspec
@@ -0,0 +1,72 @@
1
+ # SQL input plugin for Fluentd event collector
2
+
3
+ ## Overview
4
+
5
+ This sql input plugin reads records from a RDBMS periodically. Thus you can replicate tables to other storages through Fluentd.
6
+
7
+ ## How does it work?
8
+
9
+ This plugin runs following SQL repeatedly every 60 seconds to *tail* a table like `tail` command of UNIX.
10
+
11
+ SELECT * FROM *table* WHERE *update\_column* > *last\_update\_column\_value* ORDER BY *update_column* ASC LIMIT 500
12
+
13
+ What you need to configure is *update\_column*. The column needs to be updated every time when you update the row so that this plugin detects newly updated rows. Generally, the column is a timestamp such as `updated_at`.
14
+ If you omit to set the column, it uses primary key. And this plugin can't detect updated but it only reads newly inserted rows.
15
+
16
+ It stores last selected rows to a file named state\_file to not forget the last row when fluentd restarted.
17
+
18
+ ## Configuration
19
+
20
+ <source>
21
+ type sql
22
+
23
+ host rdb_host
24
+ database rdb_database
25
+ adapter mysql2_or_postgresql_etc
26
+ user myusername
27
+ password mypassword
28
+
29
+ tag_prefix my.rdb
30
+
31
+ select_interval 60s
32
+ select_limit 500
33
+
34
+ state_file /var/run/fluentd/sql_state
35
+
36
+ <table>
37
+ tag table1
38
+ table table1
39
+ update_column update_col1
40
+ time_column time_col2
41
+ </table>
42
+
43
+ <table>
44
+ tag table2
45
+ table table2
46
+ update_column updated_at
47
+ time_column updated_at
48
+ </table>
49
+
50
+ # detects all tables instead of <table> sections
51
+ #all_tables
52
+ </source>
53
+
54
+ * **host** RDBMS host
55
+ * **port** RDBMS port
56
+ * **database** RDBMS database name
57
+ * **adapter** RDBMS driver name (mysql2 for MySQL, postgresql for PostgreSQL, etc.)
58
+ * **user** RDBMS login user name
59
+ * **password** RDBMS login password
60
+ * **tag_prefix** prefix of tags of events. actual tag will be this\_tag\_prefix.tables\_tag (optional)
61
+ * **select_interval** interval to run SQLs (optional)
62
+ * **select_limit** LIMIT of number of rows for each SQL (optional)
63
+ * **state_file** path to a file to store last rows
64
+ * **all_tables** reads all tables instead of configuring each tables in \<table\> sections
65
+
66
+ \<table\> sections:
67
+
68
+ * **tag** tag name of events (optional; default value is table name)
69
+ * **table** RDBM table name
70
+ * **update_column**
71
+ * **time_column** (optional)
72
+
@@ -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.2.1
@@ -0,0 +1,24 @@
1
+ # encoding: utf-8
2
+ $:.push File.expand_path('../lib', __FILE__)
3
+
4
+ Gem::Specification.new do |gem|
5
+ gem.name = "fluent-plugin-sql"
6
+ gem.description = "SQL input/output plugin for Fluentd event collector"
7
+ gem.homepage = "https://github.com/frsyuki/fluent-plugin-sql"
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
+
19
+ gem.add_dependency "fluentd", "~> 0.10.0"
20
+ gem.add_dependency 'activerecord', ['3.2.12']
21
+ gem.add_dependency 'mysql2', ['~> 0.3.12']
22
+ gem.add_dependency 'pg', ['~> 0.16.0']
23
+ gem.add_development_dependency "rake", ">= 0.9.2"
24
+ end
@@ -0,0 +1,218 @@
1
+ #
2
+ # Fluent
3
+ #
4
+ # Copyright (C) 2013 FURUHASHI Sadayuki
5
+ #
6
+ # Licensed under the Apache License, Version 2.0 (the "License");
7
+ # you may not use this file except in compliance with the License.
8
+ # You may obtain a copy of the License at
9
+ #
10
+ # http://www.apache.org/licenses/LICENSE-2.0
11
+ #
12
+ # Unless required by applicable law or agreed to in writing, software
13
+ # distributed under the License is distributed on an "AS IS" BASIS,
14
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
15
+ # See the License for the specific language governing permissions and
16
+ # limitations under the License.
17
+ #
18
+ module Fluent
19
+
20
+ require 'active_record'
21
+
22
+ class SQLInput < Input
23
+ Plugin.register_input('sql', self)
24
+
25
+ config_param :host, :string
26
+ config_param :port, :integer, :default => nil
27
+ config_param :adapter, :string
28
+ config_param :database, :string
29
+ config_param :username, :string, :default => nil
30
+ config_param :password, :string, :default => nil
31
+
32
+ config_param :state_file, :string, :default => nil
33
+ config_param :tag_prefix, :string, :default => nil
34
+ config_param :select_interval, :time, :default => 60
35
+ config_param :select_limit, :time, :default => 500
36
+
37
+ class TableElement
38
+ include Configurable
39
+
40
+ config_param :table, :string
41
+ config_param :tag, :string, :default => nil
42
+ config_param :update_column, :string, :default => nil
43
+ config_param :time_column, :string, :default => nil
44
+
45
+ def configure(conf)
46
+ super
47
+
48
+ unless @state_file
49
+ $log.warn "'state_file PATH' parameter is not set to a 'sql' source."
50
+ $log.warn "this parameter is highly recommended to save the last rows to resume tailing."
51
+ end
52
+ end
53
+
54
+ def init(tag_prefix, base_model)
55
+ @tag = "#{tag_prefix}.#{@tag}" if tag_prefix
56
+
57
+ table_name = @table
58
+ @model = Class.new(base_model) do
59
+ self.table_name = table_name
60
+ self.inheritance_column = '_never_use_'
61
+ end
62
+ class_name = table_name.singularize.camelize
63
+ base_model.const_set(class_name, @model)
64
+ model_name = ActiveModel::Name.new(@model, nil, class_name)
65
+ @model.define_singleton_method(:model_name) { model_name }
66
+
67
+ unless @update_column
68
+ columns = Hash[@model.columns.map {|c| [c.name, c] }]
69
+ pk = columns[@model.primary_key]
70
+ unless pk
71
+ raise "Composite primary key is not supported. Set update_column parameter to <table> section."
72
+ end
73
+ @update_column = pk.name
74
+ end
75
+ end
76
+
77
+ def emit_next_records(last_record, limit)
78
+ relation = @model
79
+ if last_record && last_update_value = last_record[@update_column]
80
+ relation = relation.where("#{@update_column} > ?", last_update_value)
81
+ end
82
+ relation = relation.order("#{@update_column} ASC").limit(limit)
83
+
84
+ now = Engine.now
85
+ entry_name = @model.table_name.singularize
86
+
87
+ me = MultiEventStream.new
88
+ relation.each do |obj|
89
+ record = obj.as_json[entry_name] rescue nil
90
+ if record
91
+ if tv = record[@time_column]
92
+ time = Time.parse(tv.to_s) rescue now
93
+ else
94
+ time = now
95
+ end
96
+ me.add(time, record)
97
+ last_record = record
98
+ end
99
+ end
100
+
101
+ Engine.emit_stream(@tag, me)
102
+
103
+ return last_record
104
+ end
105
+ end
106
+
107
+ def configure(conf)
108
+ super
109
+
110
+ @tables = conf.elements.select {|e|
111
+ e.name == 'table'
112
+ }.map {|e|
113
+ te = TableElement.new
114
+ te.configure(e)
115
+ te
116
+ }
117
+
118
+ if config['all_tables']
119
+ @all_tables = true
120
+ end
121
+ end
122
+
123
+ SKIP_TABLE_REGEXP = /\Aschema_migrations\Z/i
124
+
125
+ def start
126
+ @state_store = StateStore.new(@state_file)
127
+
128
+ config = {
129
+ :adapter => @adapter,
130
+ :host => @host,
131
+ :port => @port,
132
+ :database => @database,
133
+ :username => @username,
134
+ :password => @password,
135
+ }
136
+
137
+ @base_model = Class.new(ActiveRecord::Base) do
138
+ self.abstract_class = true
139
+ end
140
+ SQLInput.const_set("BaseModel_#{rand(1<<31)}", @base_model)
141
+ @base_model.establish_connection(config)
142
+
143
+ if @all_tables
144
+ @tables = @base_model.connection.tables.map do |table_name|
145
+ if table_name.match(SKIP_TABLE_REGEXP)
146
+ nil
147
+ else
148
+ te = TableElement.new
149
+ te.configure({
150
+ 'table' => table_name,
151
+ 'tag' => table_name,
152
+ 'update_column' => nil,
153
+ })
154
+ te
155
+ end
156
+ end.compact
157
+ end
158
+
159
+ @tables.reject! do |te|
160
+ begin
161
+ te.init(@tag_prefix, @base_model)
162
+ $log.info "Selecting '#{te.table}' table"
163
+ false
164
+ rescue
165
+ $log.warn "Can't handle '#{te.table}' table. Ignoring.", :error => $!
166
+ $log.warn_backtrace $!.backtrace
167
+ true
168
+ end
169
+ end
170
+
171
+ @stop_flag = false
172
+ @thread = Thread.new(&method(:thread_main))
173
+ end
174
+
175
+ def shutdown
176
+ @stop_flag = true
177
+ end
178
+
179
+ def thread_main
180
+ until @stop_flag
181
+ sleep @select_interval
182
+
183
+ @tables.each do |t|
184
+ begin
185
+ last_record = @state_store.last_records[t.table]
186
+ @state_store.last_records[t.table] = t.emit_next_records(last_record, @select_limit)
187
+ @state_store.update!
188
+ rescue
189
+ $log.error "unexpected error", :error=>$!.to_s
190
+ $log.error_backtrace
191
+ end
192
+ end
193
+ end
194
+ end
195
+
196
+ class StateStore
197
+ def initialize(path)
198
+ @path = path
199
+ if File.exists?(@path)
200
+ @data = YAML.load_file(@path)
201
+ else
202
+ @data = {}
203
+ end
204
+ end
205
+
206
+ def last_records
207
+ @data['last_records'] ||= {}
208
+ end
209
+
210
+ def update!
211
+ File.open(@path, 'w') {|f|
212
+ f.write YAML.dump(@data)
213
+ }
214
+ end
215
+ end
216
+ end
217
+
218
+ end
metadata ADDED
@@ -0,0 +1,131 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: fluent-plugin-sql
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.2.1
5
+ prerelease:
6
+ platform: ruby
7
+ authors:
8
+ - Sadayuki Furuhashi
9
+ autorequire:
10
+ bindir: bin
11
+ cert_chain: []
12
+ date: 2013-09-04 00:00:00.000000000 Z
13
+ dependencies:
14
+ - !ruby/object:Gem::Dependency
15
+ name: fluentd
16
+ requirement: !ruby/object:Gem::Requirement
17
+ none: false
18
+ requirements:
19
+ - - ~>
20
+ - !ruby/object:Gem::Version
21
+ version: 0.10.0
22
+ type: :runtime
23
+ prerelease: false
24
+ version_requirements: !ruby/object:Gem::Requirement
25
+ none: false
26
+ requirements:
27
+ - - ~>
28
+ - !ruby/object:Gem::Version
29
+ version: 0.10.0
30
+ - !ruby/object:Gem::Dependency
31
+ name: activerecord
32
+ requirement: !ruby/object:Gem::Requirement
33
+ none: false
34
+ requirements:
35
+ - - '='
36
+ - !ruby/object:Gem::Version
37
+ version: 3.2.12
38
+ type: :runtime
39
+ prerelease: false
40
+ version_requirements: !ruby/object:Gem::Requirement
41
+ none: false
42
+ requirements:
43
+ - - '='
44
+ - !ruby/object:Gem::Version
45
+ version: 3.2.12
46
+ - !ruby/object:Gem::Dependency
47
+ name: mysql2
48
+ requirement: !ruby/object:Gem::Requirement
49
+ none: false
50
+ requirements:
51
+ - - ~>
52
+ - !ruby/object:Gem::Version
53
+ version: 0.3.12
54
+ type: :runtime
55
+ prerelease: false
56
+ version_requirements: !ruby/object:Gem::Requirement
57
+ none: false
58
+ requirements:
59
+ - - ~>
60
+ - !ruby/object:Gem::Version
61
+ version: 0.3.12
62
+ - !ruby/object:Gem::Dependency
63
+ name: pg
64
+ requirement: !ruby/object:Gem::Requirement
65
+ none: false
66
+ requirements:
67
+ - - ~>
68
+ - !ruby/object:Gem::Version
69
+ version: 0.16.0
70
+ type: :runtime
71
+ prerelease: false
72
+ version_requirements: !ruby/object:Gem::Requirement
73
+ none: false
74
+ requirements:
75
+ - - ~>
76
+ - !ruby/object:Gem::Version
77
+ version: 0.16.0
78
+ - !ruby/object:Gem::Dependency
79
+ name: rake
80
+ requirement: !ruby/object:Gem::Requirement
81
+ none: false
82
+ requirements:
83
+ - - ! '>='
84
+ - !ruby/object:Gem::Version
85
+ version: 0.9.2
86
+ type: :development
87
+ prerelease: false
88
+ version_requirements: !ruby/object:Gem::Requirement
89
+ none: false
90
+ requirements:
91
+ - - ! '>='
92
+ - !ruby/object:Gem::Version
93
+ version: 0.9.2
94
+ description: SQL input/output plugin for Fluentd event collector
95
+ email: frsyuki@gmail.com
96
+ executables: []
97
+ extensions: []
98
+ extra_rdoc_files: []
99
+ files:
100
+ - Gemfile
101
+ - README.md
102
+ - Rakefile
103
+ - VERSION
104
+ - fluent-plugin-sql.gemspec
105
+ - lib/fluent/plugin/in_sql.rb
106
+ homepage: https://github.com/frsyuki/fluent-plugin-sql
107
+ licenses: []
108
+ post_install_message:
109
+ rdoc_options: []
110
+ require_paths:
111
+ - lib
112
+ required_ruby_version: !ruby/object:Gem::Requirement
113
+ none: false
114
+ requirements:
115
+ - - ! '>='
116
+ - !ruby/object:Gem::Version
117
+ version: '0'
118
+ required_rubygems_version: !ruby/object:Gem::Requirement
119
+ none: false
120
+ requirements:
121
+ - - ! '>='
122
+ - !ruby/object:Gem::Version
123
+ version: '0'
124
+ requirements: []
125
+ rubyforge_project:
126
+ rubygems_version: 1.8.23
127
+ signing_key:
128
+ specification_version: 3
129
+ summary: SQL input/output plugin for Fluentd event collector
130
+ test_files: []
131
+ has_rdoc: false