fluent-plugin-sql-enchanced 0.5.2
Sign up to get free protection for your applications and to get access to all the features.
- 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: []
|