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 +2 -0
- data/README.md +72 -0
- data/Rakefile +14 -0
- data/VERSION +1 -0
- data/fluent-plugin-sql.gemspec +24 -0
- data/lib/fluent/plugin/in_sql.rb +218 -0
- metadata +131 -0
data/Gemfile
ADDED
data/README.md
ADDED
@@ -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
|
+
|
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.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
|