fluent-plugin-mysql-replicator 1.3.0 → 1.4.0
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
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 26f611dc962db43660ca6004ce2b634e9a863d554ff25328d7d39e1bc4c5a14e
|
|
4
|
+
data.tar.gz: dd4834e5c827564da74189882a605381fbe9be31b1755cc772ceb9bed0c09937
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: efc707ef5d9f6606baa7d573d974701bd1cedb4e34bf6a67ebda681c313b190d88ca700c505857526694b216759093cf5f834486c065074a99adb3705d5b076f
|
|
7
|
+
data.tar.gz: 17bec8dd052fb658430c1cd4d15c7731c5f7afaf27a62add1e56b6e6134f928bc5934d5db825a2918a894b0ff2f3504fe9b120ff19eaa9ca89f3c8f93d1d84b5
|
data/CHANGELOG.md
CHANGED
|
@@ -5,6 +5,25 @@ All notable changes to this project are documented in this file.
|
|
|
5
5
|
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
|
|
6
6
|
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
|
7
7
|
|
|
8
|
+
## [1.4.0] - 2026-06-17
|
|
9
|
+
|
|
10
|
+
### Added
|
|
11
|
+
- Plugin-managed Elasticsearch index templates. New `template_name`,
|
|
12
|
+
`template_file`, `template_overwrite`, and `use_legacy_template` options on
|
|
13
|
+
`mysql_replicator_elasticsearch` install an index template on startup, so newly
|
|
14
|
+
created indices — including future date-rolled ones — get the desired mapping
|
|
15
|
+
before the first document locks in dynamic mapping. This enables mappings that
|
|
16
|
+
cannot be changed after the fact: `keyword` / non-analyzed fields ([#20]) and
|
|
17
|
+
`geo_point` ([#36]). `use_legacy_template` defaults to `false` (composable
|
|
18
|
+
`PUT /_index_template`, Elasticsearch >= 7.8); set it to `true` for the legacy
|
|
19
|
+
`PUT /_template` API (Elasticsearch 6.x+).
|
|
20
|
+
|
|
21
|
+
### Documentation
|
|
22
|
+
- Document that MySQL `DECIMAL` columns are emitted as strings (because
|
|
23
|
+
`BigDecimal` cannot cross Fluentd's msgpack buffer, which also preserves exact
|
|
24
|
+
precision) and should be mapped as `double` or `scaled_float` in an index
|
|
25
|
+
template, where Elasticsearch coerces the numeric string at index time. ([#36])
|
|
26
|
+
|
|
8
27
|
## [1.3.0] - 2026-06-16
|
|
9
28
|
|
|
10
29
|
### Added
|
|
@@ -85,6 +104,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|
|
85
104
|
- First 1.0 release, targeting the Fluentd v0.14+ plugin API. Earlier 0.x
|
|
86
105
|
history is available in the git log.
|
|
87
106
|
|
|
107
|
+
[1.4.0]: https://github.com/y-ken/fluent-plugin-mysql-replicator/compare/v1.3.0...v1.4.0
|
|
88
108
|
[1.3.0]: https://github.com/y-ken/fluent-plugin-mysql-replicator/compare/v1.2.0...v1.3.0
|
|
89
109
|
[1.2.0]: https://github.com/y-ken/fluent-plugin-mysql-replicator/compare/v1.1.0...v1.2.0
|
|
90
110
|
[1.1.0]: https://github.com/y-ken/fluent-plugin-mysql-replicator/compare/v1.0.3...v1.1.0
|
|
@@ -96,7 +116,9 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|
|
96
116
|
[#4]: https://github.com/y-ken/fluent-plugin-mysql-replicator/issues/4
|
|
97
117
|
[#7]: https://github.com/y-ken/fluent-plugin-mysql-replicator/pull/7
|
|
98
118
|
[#18]: https://github.com/y-ken/fluent-plugin-mysql-replicator/pull/18
|
|
119
|
+
[#20]: https://github.com/y-ken/fluent-plugin-mysql-replicator/issues/20
|
|
99
120
|
[#27]: https://github.com/y-ken/fluent-plugin-mysql-replicator/issues/27
|
|
121
|
+
[#36]: https://github.com/y-ken/fluent-plugin-mysql-replicator/issues/36
|
|
100
122
|
[#39]: https://github.com/y-ken/fluent-plugin-mysql-replicator/pull/39
|
|
101
123
|
[#40]: https://github.com/y-ken/fluent-plugin-mysql-replicator/issues/40
|
|
102
124
|
[#42]: https://github.com/y-ken/fluent-plugin-mysql-replicator/issues/42
|
data/README.md
CHANGED
|
@@ -169,6 +169,70 @@ as before.
|
|
|
169
169
|
This applies to `mysql_replicator`; `mysql_replicator_multi` still expects a
|
|
170
170
|
single-column primary key.
|
|
171
171
|
|
|
172
|
+
## Index templates (mappings)
|
|
173
|
+
|
|
174
|
+
By default Elasticsearch infers field types from the first document (dynamic
|
|
175
|
+
mapping), and an existing field's mapping cannot be changed afterwards. To
|
|
176
|
+
control a field's mapping — e.g. `keyword` for a non-analyzed field, or
|
|
177
|
+
`geo_point`, which dynamic mapping never infers — install an **index template**
|
|
178
|
+
so it is applied to indices *before* the first document is written.
|
|
179
|
+
|
|
180
|
+
`mysql_replicator_elasticsearch` can install a template on startup. The option
|
|
181
|
+
names mirror `fluent-plugin-elasticsearch` (the default here uses the modern
|
|
182
|
+
composable API, whereas fluent-plugin-elasticsearch defaults to legacy):
|
|
183
|
+
|
|
184
|
+
```
|
|
185
|
+
<match replicator.**>
|
|
186
|
+
@type mysql_replicator_elasticsearch
|
|
187
|
+
# ...
|
|
188
|
+
template_name myindex_template
|
|
189
|
+
template_file /etc/fluent/myindex_template.json
|
|
190
|
+
template_overwrite false # set true to replace an existing template
|
|
191
|
+
use_legacy_template false # default. false: PUT /_index_template (ES >= 7.8); true: PUT /_template (legacy, ES 6.x+)
|
|
192
|
+
</match>
|
|
193
|
+
```
|
|
194
|
+
|
|
195
|
+
`template_name` and `template_file` must be set together. The template is a
|
|
196
|
+
**server-side rule**, so once installed it is applied automatically to every new
|
|
197
|
+
index whose name matches its `index_patterns` — **including future date-rolled
|
|
198
|
+
indices** (see *Date-based index names* above) — with no per-write work.
|
|
199
|
+
|
|
200
|
+
Example `template_file` (composable format, the default) mapping a non-analyzed
|
|
201
|
+
field as `keyword`, a coordinate field as `geo_point`, and a `DECIMAL` column as
|
|
202
|
+
a numeric (see below):
|
|
203
|
+
|
|
204
|
+
```json
|
|
205
|
+
{
|
|
206
|
+
"index_patterns": ["myindex-*"],
|
|
207
|
+
"template": {
|
|
208
|
+
"mappings": {
|
|
209
|
+
"properties": {
|
|
210
|
+
"message": { "type": "keyword" },
|
|
211
|
+
"location": { "type": "geo_point" },
|
|
212
|
+
"price": { "type": "scaled_float", "scaling_factor": 100 }
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
```
|
|
218
|
+
|
|
219
|
+
For Elasticsearch 6.x — or to reuse an existing `fluent-plugin-elasticsearch`
|
|
220
|
+
legacy template — set `use_legacy_template true` and put `settings`/`mappings`
|
|
221
|
+
at the top level (not under `template`).
|
|
222
|
+
|
|
223
|
+
### Numeric columns (DECIMAL)
|
|
224
|
+
|
|
225
|
+
MySQL `DECIMAL` columns are sent to Elasticsearch **as strings** — not because of
|
|
226
|
+
a type-inference issue, but because `BigDecimal` cannot cross Fluentd's msgpack
|
|
227
|
+
buffer between the input and output plugins, so the value is stringified (which
|
|
228
|
+
also preserves its exact precision). Under the default dynamic mapping such a
|
|
229
|
+
field is therefore indexed as text.
|
|
230
|
+
|
|
231
|
+
To index it as a number, map the field in your template as `double`, or — to keep
|
|
232
|
+
exact fixed-point precision — `scaled_float` (as `price` above). Elasticsearch's
|
|
233
|
+
`coerce` (enabled by default for numeric types) converts the numeric string to a
|
|
234
|
+
number at index time, so no value conversion is needed in the plugin.
|
|
235
|
+
|
|
172
236
|
## Output example
|
|
173
237
|
|
|
174
238
|
It is a example when detecting insert/update/delete events.
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
# -*- encoding: utf-8 -*-
|
|
2
2
|
Gem::Specification.new do |s|
|
|
3
3
|
s.name = "fluent-plugin-mysql-replicator"
|
|
4
|
-
s.version = "1.
|
|
4
|
+
s.version = "1.4.0"
|
|
5
5
|
s.authors = ["Kentaro Yoshida"]
|
|
6
6
|
s.email = ["y.ken.studio@gmail.com"]
|
|
7
7
|
s.homepage = "https://github.com/y-ken/fluent-plugin-mysql-replicator"
|
|
@@ -17,6 +17,20 @@ class Fluent::Plugin::MysqlReplicatorElasticsearchOutput < Fluent::Plugin::Outpu
|
|
|
17
17
|
config_param :username, :string, :default => nil
|
|
18
18
|
config_param :password, :string, :default => nil, :secret => true
|
|
19
19
|
|
|
20
|
+
# Optional: install an Elasticsearch index template on startup so that newly
|
|
21
|
+
# created indices (including future date-rolled ones) get the desired mapping
|
|
22
|
+
# (e.g. geo_point or keyword fields) before the first document locks in
|
|
23
|
+
# dynamic mapping. template_name and template_file must be set together.
|
|
24
|
+
#
|
|
25
|
+
# Parameter names mirror fluent-plugin-elasticsearch. The default uses the
|
|
26
|
+
# modern composable API (fluent-plugin-elasticsearch defaults to legacy):
|
|
27
|
+
# use_legacy_template false (default) -> PUT /_index_template/<name> (composable, ES >= 7.8)
|
|
28
|
+
# use_legacy_template true -> PUT /_template/<name> (legacy, ES 6.x+)
|
|
29
|
+
config_param :template_name, :string, :default => nil
|
|
30
|
+
config_param :template_file, :string, :default => nil
|
|
31
|
+
config_param :template_overwrite, :bool, :default => false
|
|
32
|
+
config_param :use_legacy_template, :bool, :default => false
|
|
33
|
+
|
|
20
34
|
config_section :buffer do
|
|
21
35
|
config_set_default :@type, DEFAULT_BUFFER_TYPE
|
|
22
36
|
end
|
|
@@ -35,12 +49,21 @@ class Fluent::Plugin::MysqlReplicatorElasticsearchOutput < Fluent::Plugin::Outpu
|
|
|
35
49
|
else
|
|
36
50
|
@tag_format = Regexp.new(conf['tag_format'])
|
|
37
51
|
end
|
|
52
|
+
|
|
53
|
+
if @template_name.nil? != @template_file.nil?
|
|
54
|
+
raise Fluent::ConfigError, "mysql_replicator_elasticsearch: 'template_name' and 'template_file' must be set together"
|
|
55
|
+
end
|
|
56
|
+
if @template_file && !File.exist?(@template_file)
|
|
57
|
+
raise Fluent::ConfigError, "mysql_replicator_elasticsearch: template_file not found: #{@template_file}"
|
|
58
|
+
end
|
|
38
59
|
end
|
|
39
60
|
|
|
40
61
|
def start
|
|
41
62
|
super
|
|
42
63
|
# nil means "not yet detected"; resolved on the first write.
|
|
43
64
|
@suppress_type = nil
|
|
65
|
+
@es_version = nil
|
|
66
|
+
@template_installed = false
|
|
44
67
|
end
|
|
45
68
|
|
|
46
69
|
def format(tag, time, record)
|
|
@@ -61,6 +84,7 @@ class Fluent::Plugin::MysqlReplicatorElasticsearchOutput < Fluent::Plugin::Outpu
|
|
|
61
84
|
|
|
62
85
|
def write(chunk)
|
|
63
86
|
detect_type_suppression if @suppress_type.nil?
|
|
87
|
+
install_template_once if @template_name
|
|
64
88
|
|
|
65
89
|
bulk_message = []
|
|
66
90
|
|
|
@@ -123,25 +147,76 @@ class Fluent::Plugin::MysqlReplicatorElasticsearchOutput < Fluent::Plugin::Outpu
|
|
|
123
147
|
end
|
|
124
148
|
|
|
125
149
|
# Mapping types were removed in Elasticsearch 8.x and deprecated in 7.x.
|
|
126
|
-
# Detect the
|
|
150
|
+
# Detect the version once and omit "_type" for 7.x and later.
|
|
127
151
|
def detect_type_suppression
|
|
128
|
-
|
|
152
|
+
@es_version = elasticsearch_version
|
|
153
|
+
major = @es_version&.first
|
|
129
154
|
@suppress_type = !major.nil? && major >= 7
|
|
130
155
|
if major
|
|
131
|
-
log.info "mysql_replicator_elasticsearch: detected Elasticsearch #{
|
|
156
|
+
log.info "mysql_replicator_elasticsearch: detected Elasticsearch #{@es_version.join('.')}, suppress_type=#{@suppress_type}"
|
|
132
157
|
else
|
|
133
158
|
log.warn "mysql_replicator_elasticsearch: could not detect Elasticsearch version, sending '_type' (assuming 6.x)"
|
|
134
159
|
end
|
|
135
160
|
end
|
|
136
161
|
|
|
137
|
-
|
|
162
|
+
# Returns the Elasticsearch version as [major, minor], or nil if undetectable.
|
|
163
|
+
def elasticsearch_version
|
|
138
164
|
request = Net::HTTP::Get.new('/')
|
|
139
165
|
request.basic_auth(@username, @password) if @username && @password
|
|
140
166
|
response = new_http.request(request)
|
|
141
167
|
number = Yajl::Parser.parse(response.body).dig('version', 'number')
|
|
142
|
-
number.
|
|
168
|
+
return nil if number.nil?
|
|
169
|
+
number.to_s.split('.').first(2).map(&:to_i)
|
|
143
170
|
rescue => e
|
|
144
171
|
log.warn "mysql_replicator_elasticsearch: version detection failed: #{e.message}"
|
|
145
172
|
nil
|
|
146
173
|
end
|
|
174
|
+
|
|
175
|
+
# Install the configured index template once, on the first write. The template
|
|
176
|
+
# is a server-side rule, so Elasticsearch applies it to every new index whose
|
|
177
|
+
# name matches its index_patterns -- including future date-rolled indices --
|
|
178
|
+
# without any further action here. Failures are logged but never abort indexing.
|
|
179
|
+
def install_template_once
|
|
180
|
+
return if @template_installed
|
|
181
|
+
@template_installed = true
|
|
182
|
+
install_template
|
|
183
|
+
rescue => e
|
|
184
|
+
log.warn "mysql_replicator_elasticsearch: failed to install index template '#{@template_name}': #{e.message}"
|
|
185
|
+
end
|
|
186
|
+
|
|
187
|
+
def install_template
|
|
188
|
+
if !@use_legacy_template && !composable_templates_supported?
|
|
189
|
+
log.warn "mysql_replicator_elasticsearch: composable index templates require Elasticsearch >= 7.8; skipping template '#{@template_name}' (detected #{@es_version&.join('.') || 'unknown'}). Set 'use_legacy_template true' for older Elasticsearch."
|
|
190
|
+
return
|
|
191
|
+
end
|
|
192
|
+
if !@template_overwrite && template_exists?
|
|
193
|
+
log.info "mysql_replicator_elasticsearch: index template '#{@template_name}' already exists; skipping (set 'template_overwrite true' to replace)"
|
|
194
|
+
return
|
|
195
|
+
end
|
|
196
|
+
put_template(File.read(@template_file))
|
|
197
|
+
log.info "mysql_replicator_elasticsearch: installed index template '#{@template_name}' (#{@use_legacy_template ? 'legacy' : 'composable'})"
|
|
198
|
+
end
|
|
199
|
+
|
|
200
|
+
def composable_templates_supported?
|
|
201
|
+
return false if @es_version.nil?
|
|
202
|
+
major, minor = @es_version
|
|
203
|
+
major > 7 || (major == 7 && minor >= 8)
|
|
204
|
+
end
|
|
205
|
+
|
|
206
|
+
def template_path
|
|
207
|
+
@use_legacy_template ? "/_template/#{@template_name}" : "/_index_template/#{@template_name}"
|
|
208
|
+
end
|
|
209
|
+
|
|
210
|
+
def template_exists?
|
|
211
|
+
request = Net::HTTP::Get.new(template_path)
|
|
212
|
+
request.basic_auth(@username, @password) if @username && @password
|
|
213
|
+
new_http.request(request).code.to_i == 200
|
|
214
|
+
end
|
|
215
|
+
|
|
216
|
+
def put_template(body)
|
|
217
|
+
request = Net::HTTP::Put.new(template_path, {'content-type' => 'application/json; charset=utf-8'})
|
|
218
|
+
request.basic_auth(@username, @password) if @username && @password
|
|
219
|
+
request.body = body
|
|
220
|
+
new_http.request(request).value
|
|
221
|
+
end
|
|
147
222
|
end
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
require 'helper'
|
|
2
2
|
require 'fluent/test/driver/output'
|
|
3
3
|
require 'webmock/test_unit'
|
|
4
|
+
require 'tempfile'
|
|
4
5
|
|
|
5
6
|
WebMock.disable_net_connect!
|
|
6
7
|
|
|
@@ -13,6 +14,19 @@ class MysqlReplicatorElasticsearchOutput < Test::Unit::TestCase
|
|
|
13
14
|
@tag = 'myindex.mytype.insert.id'
|
|
14
15
|
end
|
|
15
16
|
|
|
17
|
+
def teardown
|
|
18
|
+
WebMock.reset!
|
|
19
|
+
(@tmpfiles || []).each {|f| f.close! rescue nil }
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
def write_template_file(body = '{"index_patterns":["myindex-*"],"mappings":{"properties":{"loc":{"type":"geo_point"}}}}')
|
|
23
|
+
file = Tempfile.new(['tmpl', '.json'])
|
|
24
|
+
file.write(body)
|
|
25
|
+
file.flush
|
|
26
|
+
(@tmpfiles ||= []) << file
|
|
27
|
+
file.path
|
|
28
|
+
end
|
|
29
|
+
|
|
16
30
|
def driver(conf='')
|
|
17
31
|
@driver ||= Fluent::Test::Driver::Output.new(Fluent::Plugin::MysqlReplicatorElasticsearchOutput).configure(conf)
|
|
18
32
|
end
|
|
@@ -187,6 +201,49 @@ class MysqlReplicatorElasticsearchOutput < Test::Unit::TestCase
|
|
|
187
201
|
}
|
|
188
202
|
end
|
|
189
203
|
|
|
204
|
+
def test_installs_composable_template_by_default
|
|
205
|
+
stub_elastic
|
|
206
|
+
stub_elastic_version("http://localhost:9200/", "8.18.0")
|
|
207
|
+
stub_request(:get, "http://localhost:9200/_index_template/my_tmpl").to_return(:status => 404)
|
|
208
|
+
put_tmpl = stub_request(:put, "http://localhost:9200/_index_template/my_tmpl").to_return(:status => 200, :body => "{}")
|
|
209
|
+
driver.configure("template_name my_tmpl\ntemplate_file #{write_template_file}\n")
|
|
210
|
+
driver.run(default_tag: @tag) { driver.feed(sample_record) }
|
|
211
|
+
assert_requested(put_tmpl)
|
|
212
|
+
end
|
|
213
|
+
|
|
214
|
+
def test_installs_legacy_template_when_enabled
|
|
215
|
+
stub_elastic # version 6.8.23
|
|
216
|
+
stub_request(:get, "http://localhost:9200/_template/my_tmpl").to_return(:status => 404)
|
|
217
|
+
put_tmpl = stub_request(:put, "http://localhost:9200/_template/my_tmpl").to_return(:status => 200, :body => "{}")
|
|
218
|
+
driver.configure("template_name my_tmpl\ntemplate_file #{write_template_file}\nuse_legacy_template true\n")
|
|
219
|
+
driver.run(default_tag: @tag) { driver.feed(sample_record) }
|
|
220
|
+
assert_requested(put_tmpl)
|
|
221
|
+
end
|
|
222
|
+
|
|
223
|
+
def test_skips_composable_template_on_old_elasticsearch
|
|
224
|
+
stub_elastic # version 6.8.23 (< 7.8), default composable
|
|
225
|
+
put_tmpl = stub_request(:put, "http://localhost:9200/_index_template/my_tmpl")
|
|
226
|
+
driver.configure("template_name my_tmpl\ntemplate_file #{write_template_file}\n")
|
|
227
|
+
driver.run(default_tag: @tag) { driver.feed(sample_record) }
|
|
228
|
+
assert_not_requested(put_tmpl)
|
|
229
|
+
end
|
|
230
|
+
|
|
231
|
+
def test_skips_template_when_it_exists_and_no_overwrite
|
|
232
|
+
stub_elastic
|
|
233
|
+
stub_elastic_version("http://localhost:9200/", "8.18.0")
|
|
234
|
+
stub_request(:get, "http://localhost:9200/_index_template/my_tmpl").to_return(:status => 200, :body => "{}")
|
|
235
|
+
put_tmpl = stub_request(:put, "http://localhost:9200/_index_template/my_tmpl")
|
|
236
|
+
driver.configure("template_name my_tmpl\ntemplate_file #{write_template_file}\n")
|
|
237
|
+
driver.run(default_tag: @tag) { driver.feed(sample_record) }
|
|
238
|
+
assert_not_requested(put_tmpl)
|
|
239
|
+
end
|
|
240
|
+
|
|
241
|
+
def test_template_name_without_file_raises
|
|
242
|
+
assert_raise(Fluent::ConfigError) do
|
|
243
|
+
driver.configure("template_name my_tmpl\n")
|
|
244
|
+
end
|
|
245
|
+
end
|
|
246
|
+
|
|
190
247
|
def test_writes_to_https_host
|
|
191
248
|
driver.configure("ssl true\n")
|
|
192
249
|
elastic_request = stub_elastic("https://localhost:9200/_bulk")
|
metadata
CHANGED
|
@@ -1,14 +1,14 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: fluent-plugin-mysql-replicator
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 1.
|
|
4
|
+
version: 1.4.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Kentaro Yoshida
|
|
8
8
|
autorequire:
|
|
9
9
|
bindir: bin
|
|
10
10
|
cert_chain: []
|
|
11
|
-
date: 2026-06-
|
|
11
|
+
date: 2026-06-17 00:00:00.000000000 Z
|
|
12
12
|
dependencies:
|
|
13
13
|
- !ruby/object:Gem::Dependency
|
|
14
14
|
name: rake
|