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: 50b2ac9d97e49b3febf1c20fc98c815b3d6a919d299a24511a72b8b1637d5585
4
- data.tar.gz: f39b735a34afd31a77f5e82339c449265dd4ef455456a85e2902dab91db79130
3
+ metadata.gz: 26f611dc962db43660ca6004ce2b634e9a863d554ff25328d7d39e1bc4c5a14e
4
+ data.tar.gz: dd4834e5c827564da74189882a605381fbe9be31b1755cc772ceb9bed0c09937
5
5
  SHA512:
6
- metadata.gz: d57b725ee936f2ffc8ae55631e75759c68de863022cb5e5740be87eae6a756b9ddc5a531825bb603a02f6a7f57752ce75d389ff3fd777ba86e5126f83f392710
7
- data.tar.gz: 8b644659039eae41a70dbe27f206673fb1420372321c00af802925c52b7d8fe3b5abb880d0aaf059521ceb80fbd414c1a41a7b4fef6c6dd8fd2a05ef746fdf05
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.3.0"
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 major version once and omit "_type" for 7.x and later.
150
+ # Detect the version once and omit "_type" for 7.x and later.
127
151
  def detect_type_suppression
128
- major = elasticsearch_major_version
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 #{major}.x, suppress_type=#{@suppress_type}"
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
- def elasticsearch_major_version
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.to_s.split('.').first.to_i
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.3.0
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-16 00:00:00.000000000 Z
11
+ date: 2026-06-17 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: rake