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 +7 -0
- data/.travis.yml +32 -0
- data/Gemfile +2 -0
- data/Gemfile.v0.12 +6 -0
- data/README.md +148 -0
- data/Rakefile +14 -0
- data/VERSION +1 -0
- data/fluent-plugin-sql.gemspec +28 -0
- data/lib/fluent/plugin/in_sql.rb +194 -0
- data/lib/fluent/plugin/out_sql.rb +261 -0
- data/test/fixtures/schema.rb +24 -0
- data/test/helper.rb +8 -0
- data/test/plugin/test_in_sql.rb +86 -0
- data/test/plugin/test_out_sql.rb +114 -0
- metadata +174 -0
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
data/Gemfile.v0.12
ADDED
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 <table> 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,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: []
|