fluent-plugin-sql 0.2.1

Sign up to get free protection for your applications and to get access to all the features.
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