fluent-plugin-record-demux 0.2.0 → 0.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: c9da92e93a04e15799949041055f91a9d9ff9327627028ce85322343778311bf
4
- data.tar.gz: 7eb377c975bdd3bcdbb192dd43a89af9995292dadc5bcfad3174f8f1f580ca09
3
+ metadata.gz: 3c3c56b3996c80096423cb21c058f778c52ef98f6235cd4ea0ba1ba47236e918
4
+ data.tar.gz: 04006b6d00d241abeea57d56041816405dc17a88284e4e8409eee57668842caa
5
5
  SHA512:
6
- metadata.gz: a04985f89183aa35c8dbd0b4e72fbc430a5160a71b63eb539a25e720b11d51376f82cb87a07b6317fe89a001876a53161c78adda0151b6f51ca4b9536f826b7d
7
- data.tar.gz: 586b8c3ffc1b22d553a056fd0e3f96bf9ccada5e7d4c27a5e2b1e6ff714cc376d8c347f96278410b02dbc86c160d4abf94b05a4aa4abccc2f3da6ddb389d6181
6
+ metadata.gz: 28ea048eb175624647b564df623109a0d380de38ff4a4c8125a6b6212ff2c7ad38b80c8922d7af133a009b2acdb807cc9184423723168ccf797fd0fededa27d8
7
+ data.tar.gz: f9ebf69ccb5fa1c9d8793db3c35fcc7f656039ca3debcf7dcce0d40ae2ba98b5c8df43f080f00cf2789ae55fdbee6704ea2c5a3bab2d1ad50ee217ee57262cde
data/.rubocop.yml CHANGED
@@ -34,5 +34,8 @@ Metrics/MethodLength:
34
34
  Metrics/ParameterLists:
35
35
  Exclude: []
36
36
 
37
+ Naming/MethodParameterName:
38
+ MinNameLength: 2
39
+
37
40
  Style/Documentation:
38
41
  Enabled: false
data/README.md CHANGED
@@ -1,8 +1,17 @@
1
1
  # fluent-plugin-record-demux
2
2
 
3
- [Fluentd](https://fluentd.org/) plugin to dmux records.
3
+ [Fluentd](https://fluentd.org/) plugin to demux records.
4
4
 
5
- ## plugin - out - record_dmux
5
+
6
+ ## Table of Contents
7
+ * [plugin - out - record_demux](#plugin---out---record_demux)
8
+ * [plugin - out - record_demux_picker](#plugin---out---record_demux_picker)
9
+ * [plugin - filter - record_array demux](#plugin---filter---record_array_demux)
10
+ * [Installation](#installation)
11
+ * [Contribution](#contribution)
12
+
13
+
14
+ ## plugin - out - record_demux
6
15
 
7
16
  ### config
8
17
 
@@ -76,6 +85,110 @@ into events:
76
85
  { "c": "data_c", "tags_1": "data_1", "tags_2": "data_2" }
77
86
  ```
78
87
 
88
+
89
+ ## plugin - out - record_demux_picker
90
+
91
+ Demux record/event, by selecting nested fields for shared and demux parts.
92
+
93
+ It transforms 1 record/event to multiple records/events.
94
+
95
+ ### params
96
+
97
+ | setting | type | default | description |
98
+ |--------------------------------|-------------------------|---------|------------------------------------------------|
99
+ | tags | string | | tag to emit demux events on |
100
+ | demux_keys | hash | | nested key to demux |
101
+ | shared_keys | hash | {} | shared key to be present in each new event |
102
+ | demux_key_normalize | bool | false | format key to demux, in a key: , value: format |
103
+ | demux_key_normalize_key_name | string | key | when demux_key_normalize, key name to use |
104
+ | demux_key_normalize_value_name | string | value | when demux_key_normalize, value name to use |
105
+ | timestamp_key | string | nil | key for timestamp field (nil means skip) |
106
+ | timestamp_format | enum (iso, epochmillis) | iso | format of the time value |
107
+
108
+ ### example 1
109
+
110
+ Given this event :
111
+
112
+ ``` text
113
+ {
114
+ "label1": "value_label1",
115
+ "label2": "value_label2",
116
+ "label3": "value_label3",
117
+ "common": {
118
+ "metadata1": "value_metadata1",
119
+ "metadata2": "value_metadata2",
120
+ "metadata3": "value_metadata3",
121
+ "metadata4": "value_metadata4",
122
+ "metadata5": "value_metadata5",
123
+ "metadata6": "value_metadata6"
124
+ }
125
+ }
126
+
127
+ ```
128
+
129
+ With this conf :
130
+
131
+ ``` text
132
+ <match data>
133
+ @type record_demux_picker
134
+ tag demuxed
135
+ demux_keys $.label1:data1 , $.label2:data2 , $.label3:data3
136
+ shared_keys $.common.metadata1:description1 , $.common.metadata2:description2 , $.common.metadata3:description3
137
+ </match>
138
+ ```
139
+
140
+ It will produce :
141
+
142
+ ``` text
143
+ {"description1":"value_metadata1","description2":"value_metadata2","description3":"value_metadata3","data1":"value_label1"}
144
+ {"description1":"value_metadata1","description2":"value_metadata2","description3":"value_metadata3","data2":"value_label2"}
145
+ {"description1":"value_metadata1","description2":"value_metadata2","description3":"value_metadata3","data3":"value_label3"}
146
+
147
+ ```
148
+
149
+
150
+ ## plugin - filter - record_array_demux
151
+
152
+ Demux record/event, by specified fields when they are arrays.
153
+
154
+ It transforms 1 record/event to multiple records/events, one for each element of the arrays.
155
+
156
+ | setting | type | default | description |
157
+ |-------------|-----------------|---------|-----------------------------------------|
158
+ | demux_keys | array of string | *nil* | arrays keys to demux, computed when nil |
159
+ | shared_keys | array of string | [] | keys to be shared in new record |
160
+ | remove_keys | array of string | [] | keys to remove |
161
+
162
+ ### example 1
163
+
164
+ Given this event :
165
+
166
+ ``` text
167
+ {
168
+ "source": ["source1", "source2", "source3"],
169
+ "status": "active",
170
+ "timestamp": "2026-01-02T03:04:05.678Z"
171
+ }
172
+ ```
173
+
174
+ With this conf :
175
+
176
+ ``` text
177
+ <filter data>
178
+ @type record_array_demux
179
+ demux_keys source
180
+ </filter>
181
+ ```
182
+
183
+ It will produce :
184
+
185
+ ``` text
186
+ {"source":"source1","status":"active","timestamp": "2026-01-02T03:04:05.678Z"}
187
+ {"source":"source2","status":"active","timestamp": "2026-01-02T03:04:05.678Z"}
188
+ {"source":"source3","status":"active","timestamp": "2026-01-02T03:04:05.678Z"}
189
+ ```
190
+
191
+
79
192
  ## Installation
80
193
 
81
194
  Manual install, by executing:
@@ -86,9 +199,13 @@ Add to Gemfile with:
86
199
 
87
200
  $ bundle add fluent-plugin-record-demux
88
201
 
202
+ ## Contribution
203
+
204
+ PR WELCOME !
205
+
89
206
 
90
207
  ## Copyright
91
208
 
92
- * Copyright(c) 2024-2025 Thomas Tych
209
+ * Copyright(c) 2024-2026 Thomas Tych
93
210
  * License
94
211
  * Apache License, Version 2.0
data/Rakefile CHANGED
@@ -6,9 +6,15 @@ Bundler::GemHelper.install_tasks
6
6
  require 'rake/testtask'
7
7
  Rake::TestTask.new(:test) do |t|
8
8
  t.libs.push('lib', 'test')
9
- t.test_files = FileList['test/**/test_*.rb', 'test/**/*_test.rb',]
10
- t.verbose = true
11
- t.warning = true
9
+
10
+ t.test_files = if ENV['TEST_FILE']
11
+ FileList["#{ENV['TEST_FILE']}*",
12
+ "test/**/#{ENV['TEST_FILE']}*"]
13
+ else
14
+ FileList['test/**/test_*.rb', 'test/**/*_test.rb']
15
+ end
16
+ t.verbose = ENV.fetch('VERBOSE', false)
17
+ t.warning = ENV.fetch('WARNING', false)
12
18
  end
13
19
 
14
20
  require 'rubocop/rake_task'
@@ -5,7 +5,7 @@ $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
5
5
 
6
6
  Gem::Specification.new do |spec|
7
7
  spec.name = 'fluent-plugin-record-demux'
8
- spec.version = '0.2.0'
8
+ spec.version = '0.4.0'
9
9
  spec.authors = ['Thomas Tych']
10
10
  spec.email = ['thomas.tych@gmail.com']
11
11
 
@@ -27,6 +27,7 @@ Gem::Specification.new do |spec|
27
27
  spec.add_development_dependency 'bump', '~> 0.10'
28
28
  spec.add_development_dependency 'bundler', '~> 2.6', '>= 2.6.5'
29
29
  spec.add_development_dependency 'byebug', '~> 11.1', '>= 11.1.3'
30
+ spec.add_development_dependency 'mocha', '~> 2.7', '>= 2.7.1'
30
31
  spec.add_development_dependency 'rake', '~> 13.2', '>= 13.2.1'
31
32
  spec.add_development_dependency 'reek', '~> 6.4'
32
33
  spec.add_development_dependency 'rubocop', '~> 1.74'
@@ -0,0 +1,103 @@
1
+ # frozen_string_literal: true
2
+
3
+ #
4
+ # Copyright 2026- Thomas Tych
5
+ #
6
+ # Licensed under the Apache License, Version 2.0 (the "License");
7
+ # you may not use this file except in compliance with the License.
8
+ # You may obtain a copy of the License at
9
+ #
10
+ # http://www.apache.org/licenses/LICENSE-2.0
11
+ #
12
+ # Unless required by applicable law or agreed to in writing, software
13
+ # distributed under the License is distributed on an "AS IS" BASIS,
14
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
15
+ # See the License for the specific language governing permissions and
16
+ # limitations under the License.
17
+
18
+ require 'fluent/plugin/filter'
19
+
20
+ module Fluent
21
+ module Plugin
22
+ class RecordArrayDemuxFilter < Fluent::Plugin::Filter
23
+ NAME = 'record_array_demux'
24
+ Fluent::Plugin.register_filter(NAME, self)
25
+
26
+ helpers :event_emitter, :timer
27
+
28
+ desc 'list of arrays keys to demux'
29
+ config_param :demux_keys, :array, value_type: :string, default: nil
30
+ desc 'list of keys to be shared in all new records'
31
+ config_param :shared_keys, :array, value_type: :string, default: nil
32
+ desc 'list of keys to be removed'
33
+ config_param :remove_keys, :array, value_type: :string, default: []
34
+
35
+ def configure(conf)
36
+ super
37
+
38
+ if demux_keys.nil? && shared_keys.nil?
39
+ raise Fluent::ConfigError, 'specify demux_keys or shared_keys'
40
+ end
41
+
42
+ true
43
+ end
44
+
45
+ def multi_workers_ready?
46
+ true
47
+ end
48
+
49
+ def filter_stream(_tag, es)
50
+ new_es = Fluent::MultiEventStream.new
51
+ es.each do |time, record|
52
+ new_records = process_record(time, record)
53
+ new_records.each { |new_record| new_es.add(time, new_record) }
54
+ end
55
+ new_es
56
+ end
57
+
58
+ def dispatch_record_keys(record)
59
+ record_keys = record.keys - remove_keys
60
+
61
+ record_shared_keys = if shared_keys.nil?
62
+ record_keys - demux_keys
63
+ else
64
+ record_keys.intersection(shared_keys)
65
+ end
66
+
67
+ record_demux_keys = if demux_keys.nil?
68
+ record_keys - record_shared_keys
69
+ else
70
+ record_keys.intersection(demux_keys)
71
+ end
72
+ real_record_demux_keys, non_record_demux_keys = record_demux_keys.partition do |key|
73
+ record[key].is_a?(Array)
74
+ end
75
+ record_shared_keys += non_record_demux_keys
76
+
77
+ [real_record_demux_keys, record_shared_keys]
78
+ end
79
+
80
+ def process_record(_time, record)
81
+ record_demux_keys, record_shared_keys = dispatch_record_keys(record)
82
+
83
+ return [record] if record_demux_keys.empty?
84
+
85
+ record_shared = record.slice(*record_shared_keys)
86
+
87
+ demux_arrays = record_demux_keys.map do |key|
88
+ array_demux_value = record[key]
89
+ array_demux_value.size.positive? ? array_demux_value : [nil]
90
+ end
91
+
92
+ demux_combinations = demux_arrays.first.product(*demux_arrays[1..])
93
+ demux_combinations.map do |combination|
94
+ new_record = record_shared.dup
95
+ record_demux_keys.each_with_index do |key, index|
96
+ new_record[key] = combination[index]
97
+ end
98
+ new_record
99
+ end
100
+ end
101
+ end
102
+ end
103
+ end
@@ -26,7 +26,7 @@ module Fluent
26
26
  helpers :event_emitter, :timer
27
27
 
28
28
  desc 'tag to emit events on'
29
- config_param :tag, :string, default: nil
29
+ config_param :tag, :string
30
30
 
31
31
  desc 'list of keys to demux'
32
32
  config_param :demux_keys, :array, value_type: :string, default: nil
@@ -0,0 +1,134 @@
1
+ # frozen_string_literal: true
2
+
3
+ #
4
+ # Copyright 2024- Thomas Tych
5
+ #
6
+ # Licensed under the Apache License, Version 2.0 (the "License");
7
+ # you may not use this file except in compliance with the License.
8
+ # You may obtain a copy of the License at
9
+ #
10
+ # http://www.apache.org/licenses/LICENSE-2.0
11
+ #
12
+ # Unless required by applicable law or agreed to in writing, software
13
+ # distributed under the License is distributed on an "AS IS" BASIS,
14
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
15
+ # See the License for the specific language governing permissions and
16
+ # limitations under the License.
17
+
18
+ require 'fluent/plugin/output'
19
+
20
+ module Fluent
21
+ module Plugin
22
+ class RecordDemuxPickerOutput < Fluent::Plugin::Output
23
+ NAME = 'record_demux_picker'
24
+ Fluent::Plugin.register_output(NAME, self)
25
+
26
+ DEMUX_KEY_NORMALIZE_KEY_NAME = 'key'
27
+ DEMUX_KEY_NORMALIZE_VALUE_NAME = 'value'
28
+
29
+ helpers :event_emitter, :timer, :record_accessor
30
+
31
+ desc 'tag to emit events on'
32
+ config_param :tag, :string
33
+
34
+ desc 'list of keys to demux'
35
+ config_param :demux_keys, :hash
36
+ desc 'list of keys to be shared in all new events'
37
+ config_param :shared_keys, :hash, default: {}
38
+
39
+ desc 'normalize demux key format'
40
+ config_param :demux_key_normalize, :bool, default: false
41
+ desc 'demux key normalize key name'
42
+ config_param :demux_key_normalize_key_name, :string, default: DEMUX_KEY_NORMALIZE_KEY_NAME
43
+ desc 'demux key normalize value name'
44
+ config_param :demux_key_normalize_value_name, :string, default: DEMUX_KEY_NORMALIZE_VALUE_NAME
45
+
46
+ desc 'timestamp key'
47
+ config_param :timestamp_key, :string, default: nil
48
+ desc 'timestamp format'
49
+ config_param :timestamp_format, :enum, list: %i[iso epochmillis], default: :iso
50
+
51
+ def configure(conf)
52
+ super
53
+
54
+ @demux_keys_mappers = @demux_keys.map { |key, target| KeyMapper.new(key, target) }
55
+ @shared_keys_mappers = @shared_keys.map { |key, target| KeyMapper.new(key, target) }
56
+
57
+ true
58
+ end
59
+
60
+ def multi_workers_ready?
61
+ true
62
+ end
63
+
64
+ def process(_events_tag, events)
65
+ demux_events = MultiEventStream.new
66
+ events.each do |time, event|
67
+ new_events = process_event(time, event)
68
+ new_events.each { |new_event| demux_events.add(time, new_event) }
69
+ end
70
+ router.emit_stream(tag, demux_events)
71
+ end
72
+
73
+ def process_event(time, event)
74
+ shared_event = extract_shared_event(event)
75
+
76
+ @demux_keys_mappers.map do |mapper|
77
+ value = mapper.accessor.call(event)
78
+ shared_event
79
+ .merge(format_demux_key(mapper.target, value))
80
+ .merge(format_time(time))
81
+ rescue StandardError => e
82
+ log.warn "#{NAME} : failure while processing event : #{e}"
83
+ nil
84
+ end.compact
85
+ end
86
+
87
+ private
88
+
89
+ def extract_shared_event(event)
90
+ @shared_keys_mappers.each_with_object({}) do |mapper, new_event|
91
+ value = mapper.accessor.call(event)
92
+ new_event[mapper.target] = value
93
+ rescue StandardError => e
94
+ log.warn "#{NAME} : failure while processing event : #{e}"
95
+ next
96
+ end
97
+ end
98
+
99
+ def format_demux_key(key, value)
100
+ if demux_key_normalize
101
+ { demux_key_normalize_key_name => key,
102
+ demux_key_normalize_value_name => value }
103
+ else
104
+ { key => value }
105
+ end
106
+ end
107
+
108
+ def format_time(time)
109
+ return {} unless timestamp_key
110
+
111
+ { timestamp_key => format_timestamp(time) }
112
+ end
113
+
114
+ def format_timestamp(time)
115
+ return (time.to_time.utc.to_f * 1000).to_i if @timestamp_format == :epochmillis
116
+
117
+ time.to_time.utc.iso8601(3)
118
+ end
119
+
120
+ class KeyMapper
121
+ include Fluent::PluginHelper::RecordAccessor
122
+
123
+ attr_reader :key, :accessor, :target, :setter
124
+
125
+ def initialize(key, target)
126
+ @key = key
127
+ @accessor = record_accessor_create(key)
128
+ @target = target
129
+ @target ||= @accessor.keys.is_a?(Array) ? @accessor.keys.last : @accessor.keys
130
+ end
131
+ end
132
+ end
133
+ end
134
+ end
metadata CHANGED
@@ -1,13 +1,13 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: fluent-plugin-record-demux
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.2.0
4
+ version: 0.4.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Thomas Tych
8
8
  bindir: bin
9
9
  cert_chain: []
10
- date: 2025-03-14 00:00:00.000000000 Z
10
+ date: 1980-01-02 00:00:00.000000000 Z
11
11
  dependencies:
12
12
  - !ruby/object:Gem::Dependency
13
13
  name: bump
@@ -63,6 +63,26 @@ dependencies:
63
63
  - - ">="
64
64
  - !ruby/object:Gem::Version
65
65
  version: 11.1.3
66
+ - !ruby/object:Gem::Dependency
67
+ name: mocha
68
+ requirement: !ruby/object:Gem::Requirement
69
+ requirements:
70
+ - - "~>"
71
+ - !ruby/object:Gem::Version
72
+ version: '2.7'
73
+ - - ">="
74
+ - !ruby/object:Gem::Version
75
+ version: 2.7.1
76
+ type: :development
77
+ prerelease: false
78
+ version_requirements: !ruby/object:Gem::Requirement
79
+ requirements:
80
+ - - "~>"
81
+ - !ruby/object:Gem::Version
82
+ version: '2.7'
83
+ - - ">="
84
+ - !ruby/object:Gem::Version
85
+ version: 2.7.1
66
86
  - !ruby/object:Gem::Dependency
67
87
  name: rake
68
88
  requirement: !ruby/object:Gem::Requirement
@@ -218,7 +238,9 @@ files:
218
238
  - README.md
219
239
  - Rakefile
220
240
  - fluent-plugin-record-demux.gemspec
241
+ - lib/fluent/plugin/filter_record_array_demux.rb
221
242
  - lib/fluent/plugin/out_record_demux.rb
243
+ - lib/fluent/plugin/out_record_demux_picker.rb
222
244
  homepage: https://gitlab.com/ttych/fluent-plugin-record-demux
223
245
  licenses:
224
246
  - Apache-2.0
@@ -238,7 +260,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
238
260
  - !ruby/object:Gem::Version
239
261
  version: '0'
240
262
  requirements: []
241
- rubygems_version: 3.6.5
263
+ rubygems_version: 4.0.3
242
264
  specification_version: 4
243
265
  summary: fluentd plugin to demux record by keys.
244
266
  test_files: []