logstash-codec-multiline 2.0.2 → 2.0.3

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: f2a90b45c2f55e7db24a9f2af3905b82d18bba6c
4
- data.tar.gz: 27cdceb50b94688c6902d10593db9f2ff4db33d3
3
+ metadata.gz: b89e4a5a454c8bc979aca1a959c06ad2d65e29bc
4
+ data.tar.gz: 3eff2f29480107e6212eec95c096c18b3f82b55e
5
5
  SHA512:
6
- metadata.gz: d040c49a3fba07cd543e1eedc4a608c90c03bd5106e7c431fa85425638d3ce05d5c43a4c00289af0a531f819ca2034a5e57d6bd127df75af556562b22985fefa
7
- data.tar.gz: 5ff667aa293cc3bdbfeba833ede8127041f7eb81b123a9a6a4afd641f9092d8452e570d6bd8b517fcbb88873a0d768d6e8852b0a9e2330ca2bcf867ccbf45cfc
6
+ metadata.gz: d7eb64acd24053b8ce18bfe111a5ab2917f5a41a3e979879ee1313bc64ffcf744ed85d11ade9843167bfe86579a34b8015d62b5fa0ee85d1356a1ac3360d246e
7
+ data.tar.gz: 36580542f95f5b66d042f8a2e6ed69064b49a1e2c1e2400f0aa49bfeb8f54ec510b99646fc1758d3a9887fb41862d1fd7ad44c30cbf3be721bf73323221953af
data/CHANGELOG.md CHANGED
@@ -1,5 +1,8 @@
1
+ ## 2.0.3
2
+ - Add pseudo codec IdentityMapCodec. Support class for identity based multiline processing.
3
+
1
4
  ## 2.0.0
2
- - Plugins were updated to follow the new shutdown semantic, this mainly allows Logstash to instruct input plugins to terminate gracefully,
5
+ - Plugins were updated to follow the new shutdown semantic, this mainly allows Logstash to instruct input plugins to terminate gracefully,
3
6
  instead of using Thread.raise on the plugins' threads. Ref: https://github.com/elastic/logstash/pull/3895
4
7
  - Dependency on logstash-core update to 2.0
5
8
 
data/README.md CHANGED
@@ -1,5 +1,8 @@
1
1
  # Logstash Plugin
2
2
 
3
+ [![Build
4
+ Status](http://build-eu-00.elastic.co/view/LS%20Plugins/view/LS%20Codecs/job/logstash-plugin-codec-multiline-unit/badge/icon)](http://build-eu-00.elastic.co/view/LS%20Plugins/view/LS%20Codecs/job/logstash-plugin-codec-multiline-unit/)
5
+
3
6
  This is a plugin for [Logstash](https://github.com/elastic/logstash).
4
7
 
5
8
  It is fully free and fully open source. The license is Apache 2.0, meaning you are pretty much free to use it however you want in whatever way.
@@ -0,0 +1,251 @@
1
+ # encoding: utf-8
2
+ require "logstash/namespace"
3
+ require "thread_safe"
4
+
5
+ # This class is a Codec duck type
6
+ # Using Composition, it maps from a stream identity to
7
+ # a cloned codec instance via the same API as a Codec
8
+ # it implements the codec public API
9
+
10
+ module LogStash module Codecs class IdentityMapCodec
11
+ # subclass of Exception, LS has more than limit (20000) active streams
12
+ class IdentityMapUpperLimitException < Exception; end
13
+
14
+ module EightyPercentWarning
15
+ extend self
16
+ def visit(imc)
17
+ current_size, limit = imc.current_size_and_limit
18
+ return if current_size < (limit * 0.8)
19
+ imc.logger.warn("IdentityMapCodec has reached 80% capacity",
20
+ :current_size => current_size, :upper_limit => limit)
21
+ end
22
+ end
23
+
24
+ module UpperLimitReached
25
+ extend self
26
+ def visit(imc)
27
+ current_size, limit = imc.current_size_and_limit
28
+ return if current_size < limit
29
+ # we hit the limit
30
+ # try to clean out stale streams
31
+ current_size, limit = imc.map_cleanup
32
+ return if current_size < limit
33
+ # we are still at the limit and all streams are in use
34
+ imc.logger.error("IdentityMapCodec has reached 100% capacity",
35
+ :current_size => current_size, :upper_limit => limit)
36
+ raise IdentityMapUpperLimitException.new
37
+ end
38
+ end
39
+
40
+ class MapCleaner
41
+ def initialize(imc, interval)
42
+ @imc, @interval = imc, interval
43
+ @running = false
44
+ end
45
+
46
+ def start
47
+ return self if running?
48
+ @running = true
49
+ @thread = Thread.new(@imc) do |imc|
50
+ loop do
51
+ sleep @interval
52
+ break if !@running
53
+ imc.map_cleanup
54
+ end
55
+ end
56
+ self
57
+ end
58
+
59
+ def running?
60
+ @running
61
+ end
62
+
63
+ def stop
64
+ return if !running?
65
+ @running = false
66
+ @thread.wakeup
67
+ end
68
+ end
69
+
70
+ # A composite class to hold both the codec and the eviction_timeout
71
+ # instances of this Value Object are stored in the mapping hash
72
+ class CodecValue
73
+ attr_reader :codec
74
+ attr_accessor :timeout
75
+
76
+ def initialize(codec)
77
+ @codec = codec
78
+ end
79
+ end
80
+
81
+ #maximum size of the mapping hash
82
+ MAX_IDENTITIES = 20_000
83
+
84
+ # time after which a stream is
85
+ # considered stale
86
+ # each time a stream is accessed
87
+ # it is given a new timeout
88
+ EVICT_TIMEOUT = 60 * 60 * 1 # 1 hour
89
+
90
+ # time that the cleaner thread sleeps for
91
+ # before it tries to clean out stale mappings
92
+ CLEANER_INTERVAL = 60 * 5 # 5 minutes
93
+
94
+ attr_reader :identity_map
95
+ attr_accessor :base_codec, :cleaner
96
+
97
+ def initialize(codec)
98
+ @base_codec = codec
99
+ @base_codecs = [codec]
100
+ @identity_map = ThreadSafe::Hash.new &method(:codec_builder)
101
+ @max_identities = MAX_IDENTITIES
102
+ @evict_timeout = EVICT_TIMEOUT
103
+ @cleaner = MapCleaner.new(self, CLEANER_INTERVAL)
104
+ @decode_block = lambda {|*| }
105
+ end
106
+
107
+ # ==============================================
108
+ # Constructional/builder methods
109
+ # chain this method off of new
110
+ #
111
+ # used to add a non-default maximum identities
112
+ def max_identities(max)
113
+ @max_identities = max.to_i
114
+ self
115
+ end
116
+
117
+ # used to add a non-default evict timeout
118
+ def evict_timeout(timeout)
119
+ @evict_timeout = timeout.to_i
120
+ self
121
+ end
122
+
123
+ # used to add a non-default cleaner interval
124
+ def cleaner_interval(interval)
125
+ @cleaner.stop
126
+ @cleaner = MapCleaner.new(self, interval.to_i)
127
+ self
128
+ end
129
+ # end Constructional/builder methods
130
+ # ==============================================
131
+
132
+ # ==============================================
133
+ # Codec API
134
+ def decode(data, identity = nil, &block)
135
+ @decode_block = block if @decode_block != block
136
+ stream_codec(identity).decode(data, &block)
137
+ end
138
+
139
+ alias_method :<<, :decode
140
+
141
+ def encode(event, identity = nil)
142
+ stream_codec(identity).encode(event)
143
+ end
144
+
145
+ # this method will not be called from
146
+ # the input or the pipeline unless
147
+ # we implement codec flush on shutdown
148
+ # problematic, because we may not have
149
+ # received all the multiline parts yet.
150
+ # but if we don't flush we will lose data
151
+ def flush(&block)
152
+ all_codecs.each do |codec|
153
+ #let ruby do its default args thing
154
+ block.nil? ? codec.flush : codec.flush(&block)
155
+ end
156
+ end
157
+
158
+ def close()
159
+ cleaner.stop
160
+ all_codecs.each(&:close)
161
+ end
162
+ # end Codec API
163
+ # ==============================================
164
+
165
+ def all_codecs
166
+ no_streams? ? @base_codecs : identity_map.values.map(&:codec)
167
+ end
168
+
169
+ def max_limit
170
+ @max_identities
171
+ end
172
+
173
+ def identity_count
174
+ identity_map.size
175
+ end
176
+
177
+ # support cleaning of stale stream/codecs
178
+ # a stream is considered stale if it has not
179
+ # been accessed in the last @evict_timeout
180
+ # period (default 1 hour)
181
+ def map_cleanup
182
+ cut_off = Time.now.to_i
183
+ # delete_if is atomic
184
+ # contents should not mutate during this call
185
+ identity_map.delete_if do |identity, compo|
186
+ if (flag = compo.timeout <= cut_off)
187
+ compo.codec.flush(&@decode_block)
188
+ end
189
+ flag
190
+ end
191
+ current_size_and_limit
192
+ end
193
+
194
+ def current_size_and_limit
195
+ [identity_count, max_limit]
196
+ end
197
+
198
+ def logger
199
+ # we 'borrow' the codec's logger as we don't have our own
200
+ @base_codec.logger
201
+ end
202
+
203
+ def codec_without_usage_update(identity)
204
+ find_codec_value(identity).codec
205
+ end
206
+
207
+ def eviction_timestamp_for(identity)
208
+ find_codec_value(identity).timeout
209
+ end
210
+
211
+ private
212
+
213
+ def stream_codec(identity)
214
+ return base_codec if identity.nil?
215
+ record_codec_usage(identity) # returns codec
216
+ end
217
+
218
+ def find_codec_value(identity)
219
+ identity_map[identity]
220
+ end
221
+
222
+ # for nil stream this method is not called
223
+ def record_codec_usage(identity)
224
+ check_map_limits
225
+ # only start the cleaner if streams are in use
226
+ # continuous calls to start are OK
227
+ cleaner.start
228
+ compo = find_codec_value(identity)
229
+ compo.timeout = eviction_timestamp
230
+ compo.codec
231
+ end
232
+
233
+ def eviction_timestamp
234
+ Time.now.to_i + @evict_timeout
235
+ end
236
+
237
+ def check_map_limits
238
+ UpperLimitReached.visit(self)
239
+ EightyPercentWarning.visit(self)
240
+ end
241
+
242
+ def codec_builder(hash, k)
243
+ codec = hash.empty? ? @base_codec : @base_codec.clone
244
+ compo = CodecValue.new(codec)
245
+ hash.store(k, compo)
246
+ end
247
+
248
+ def no_streams?
249
+ identity_map.empty?
250
+ end
251
+ end end end
@@ -181,7 +181,7 @@ class LogStash::Codecs::Multiline < LogStash::Codecs::Base
181
181
  end
182
182
 
183
183
  def flush(&block)
184
- if @buffer.any?
184
+ if @buffer.any?
185
185
  yield merge_events
186
186
  reset_buffer
187
187
  end
@@ -211,7 +211,7 @@ class LogStash::Codecs::Multiline < LogStash::Codecs::Base
211
211
  end
212
212
 
213
213
  def over_maximun_lines?
214
- @buffer.size > @max_lines
214
+ @buffer.size > @max_lines
215
215
  end
216
216
 
217
217
  def over_maximun_bytes?
@@ -227,4 +227,4 @@ class LogStash::Codecs::Multiline < LogStash::Codecs::Base
227
227
  @on_event.call(event, event)
228
228
  end # def encode
229
229
 
230
- end # class LogStash::Codecs::Plain
230
+ end # class LogStash::Codecs::Multiline
@@ -1,7 +1,7 @@
1
1
  Gem::Specification.new do |s|
2
2
 
3
3
  s.name = 'logstash-codec-multiline'
4
- s.version = '2.0.2'
4
+ s.version = '2.0.3'
5
5
  s.licenses = ['Apache License (2.0)']
6
6
  s.summary = "The multiline codec will collapse multiline messages and merge them into a single event."
7
7
  s.description = "This gem is a logstash plugin required to be installed on top of the Logstash core pipeline using $LS_HOME/bin/plugin install gemname. This gem is not a stand-alone program"
@@ -0,0 +1,208 @@
1
+ # encoding: utf-8
2
+ require "logstash/devutils/rspec/spec_helper"
3
+ require "logstash/codecs/identity_map_codec"
4
+
5
+ class LogTracer
6
+ def initialize() @tracer = []; end
7
+ def warn(*args) @tracer.push [:warn, args]; end
8
+ def error(*args) @tracer.push [:error, args]; end
9
+
10
+ def trace_for(symbol)
11
+ params = @tracer.assoc(symbol)
12
+ params.nil? ? false : params.last
13
+ end
14
+ end
15
+
16
+ class IdentityMapCodecTracer
17
+ def initialize() @tracer = []; end
18
+ def clone() self.class.new; end
19
+ def decode(data) @tracer.push [:decode, data]; end
20
+ def encode(event) @tracer.push [:encode, event]; end
21
+ def flush(&block) @tracer.push [:flush, true]; end
22
+ def close() @tracer.push [:close, true]; end
23
+ def logger() @logger ||= LogTracer.new; end
24
+
25
+ def trace_for(symbol)
26
+ params = @tracer.assoc(symbol)
27
+ params.nil? ? false : params.last
28
+ end
29
+ end
30
+
31
+ describe LogStash::Codecs::IdentityMapCodec do
32
+ let(:codec) { IdentityMapCodecTracer.new }
33
+ let(:logger) { codec.logger }
34
+ let(:demuxer) { described_class.new(codec) }
35
+ let(:stream1) { "stream-a" }
36
+ let(:codec1) { demuxer.codec_without_usage_update(stream1) }
37
+ let(:arg1) { "data-a" }
38
+
39
+ after do
40
+ codec.close
41
+ end
42
+
43
+ describe "operating without stream identity" do
44
+ let(:stream1) { nil }
45
+
46
+ it "transparently refers to the original codec" do
47
+ expect(codec).to eql(codec1)
48
+ end
49
+ end
50
+
51
+ describe "operating with stream identity" do
52
+
53
+ before { demuxer.decode(arg1, stream1) }
54
+
55
+ it "the first identity refers to the original codec" do
56
+ expect(codec).to eql(codec1)
57
+ end
58
+ end
59
+
60
+ describe "#decode" do
61
+ context "when no identity is used" do
62
+ let(:stream1) { nil }
63
+
64
+ it "calls the method on the original codec" do
65
+ demuxer.decode(arg1, stream1)
66
+
67
+ expect(codec.trace_for(:decode)).to eq(arg1)
68
+ end
69
+ end
70
+
71
+ context "when multiple identities are used" do
72
+ let(:stream2) { "stream-b" }
73
+ let(:codec2) { demuxer.codec_without_usage_update(stream2) }
74
+ let(:arg2) { "data-b" }
75
+
76
+ it "calls the method on the appropriate codec" do
77
+ demuxer.decode(arg1, stream1)
78
+ demuxer.decode(arg2, stream2)
79
+
80
+ expect(codec1.trace_for(:decode)).to eq(arg1)
81
+ expect(codec2.trace_for(:decode)).to eq(arg2)
82
+ end
83
+ end
84
+ end
85
+
86
+ describe "#encode" do
87
+ context "when no identity is used" do
88
+ let(:stream1) { nil }
89
+ let(:arg1) { LogStash::Event.new({"type" => "file"}) }
90
+
91
+ it "calls the method on the original codec" do
92
+ demuxer.encode(arg1, stream1)
93
+
94
+ expect(codec.trace_for(:encode)).to eq(arg1)
95
+ end
96
+ end
97
+
98
+ context "when multiple identities are used" do
99
+ let(:stream2) { "stream-b" }
100
+ let(:codec2) { demuxer.codec_without_usage_update(stream2) }
101
+ let(:arg2) { LogStash::Event.new({"type" => "file"}) }
102
+
103
+ it "calls the method on the appropriate codec" do
104
+ demuxer.encode(arg1, stream1)
105
+ demuxer.encode(arg2, stream2)
106
+
107
+ expect(codec1.trace_for(:encode)).to eq(arg1)
108
+ expect(codec2.trace_for(:encode)).to eq(arg2)
109
+ end
110
+ end
111
+ end
112
+
113
+ describe "#close" do
114
+ context "when no identity is used" do
115
+ before do
116
+ demuxer.decode(arg1)
117
+ end
118
+
119
+ it "calls the method on the original codec" do
120
+ demuxer.close
121
+ expect(codec.trace_for(:close)).to be_truthy
122
+ end
123
+ end
124
+
125
+ context "when multiple identities are used" do
126
+ let(:stream2) { "stream-b" }
127
+ let(:codec2) { demuxer.codec_without_usage_update(stream2) }
128
+ let(:arg2) { LogStash::Event.new({"type" => "file"}) }
129
+
130
+ before do
131
+ demuxer.decode(arg1, stream1)
132
+ demuxer.decode(arg2, stream2)
133
+ end
134
+
135
+ it "calls the method on all codecs" do
136
+ demuxer.close
137
+
138
+ expect(codec1.trace_for(:close)).to be_truthy
139
+ expect(codec2.trace_for(:close)).to be_truthy
140
+ end
141
+ end
142
+ end
143
+
144
+ describe "over capacity protection" do
145
+ let(:demuxer) { described_class.new(codec).max_identities(limit) }
146
+
147
+ context "when capacity at 80% or higher" do
148
+ let(:limit) { 10 }
149
+
150
+ it "a warning is logged" do
151
+ limit.pred.times do |i|
152
+ demuxer.decode(Object.new, "stream#{i}")
153
+ end
154
+
155
+ expect(logger.trace_for(:warn).first).to match %r|has reached 80% capacity|
156
+ end
157
+ end
158
+
159
+ context "when capacity is exceeded" do
160
+ let(:limit) { 2 }
161
+ let(:error_class) { LogStash::Codecs::IdentityMapCodec::IdentityMapUpperLimitException }
162
+
163
+ it "an exception is raised" do
164
+ limit.times do |i|
165
+ demuxer.decode(Object.new, "stream#{i}")
166
+ end
167
+ expect { demuxer.decode(Object.new, "stream4") }.to raise_error(error_class)
168
+ end
169
+
170
+ context "initially but some streams are idle and can be evicted" do
171
+ let(:demuxer) { described_class.new(codec).max_identities(limit).evict_timeout(1) }
172
+
173
+ it "an exception is NOT raised" do
174
+ demuxer.decode(Object.new, "stream1")
175
+ sleep(1.2)
176
+ demuxer.decode(Object.new, "stream2")
177
+ expect(demuxer.identity_count).to eq(limit)
178
+ expect { demuxer.decode(Object.new, "stream4") }.not_to raise_error
179
+ end
180
+ end
181
+ end
182
+ end
183
+
184
+ describe "usage tracking" do
185
+ let(:demuxer) { described_class.new(codec).evict_timeout(10) }
186
+ context "when an operation is performed by identity" do
187
+ it "the new eviction time for that identity is recorded" do
188
+ demuxer.decode(Object.new, "stream1")
189
+ current_eviction = demuxer.eviction_timestamp_for("stream1")
190
+ sleep(2)
191
+ demuxer.decode(Object.new, "stream1")
192
+ expect(demuxer.eviction_timestamp_for("stream1")).to be >= current_eviction + 2
193
+ end
194
+ end
195
+ end
196
+
197
+ describe "codec eviction" do
198
+ let(:demuxer) { described_class.new(codec).evict_timeout(1).cleaner_interval(1) }
199
+ context "when an identity has become stale" do
200
+ it "the cleaner evicts the codec and flushes it first" do
201
+ demuxer.decode(Object.new, "stream1")
202
+ sleep(2.1)
203
+ expect(codec.trace_for(:flush)).to be_truthy
204
+ expect(demuxer.identity_map.keys).not_to include("stream1")
205
+ end
206
+ end
207
+ end
208
+ end
metadata CHANGED
@@ -1,17 +1,18 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: logstash-codec-multiline
3
3
  version: !ruby/object:Gem::Version
4
- version: 2.0.2
4
+ version: 2.0.3
5
5
  platform: ruby
6
6
  authors:
7
7
  - Elastic
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2015-10-14 00:00:00.000000000 Z
11
+ date: 2015-11-11 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
- requirement: !ruby/object:Gem::Requirement
14
+ name: logstash-core
15
+ version_requirements: !ruby/object:Gem::Requirement
15
16
  requirements:
16
17
  - - '>='
17
18
  - !ruby/object:Gem::Version
@@ -19,10 +20,7 @@ dependencies:
19
20
  - - <
20
21
  - !ruby/object:Gem::Version
21
22
  version: 3.0.0
22
- name: logstash-core
23
- prerelease: false
24
- type: :runtime
25
- version_requirements: !ruby/object:Gem::Requirement
23
+ requirement: !ruby/object:Gem::Requirement
26
24
  requirements:
27
25
  - - '>='
28
26
  - !ruby/object:Gem::Version
@@ -30,64 +28,68 @@ dependencies:
30
28
  - - <
31
29
  - !ruby/object:Gem::Version
32
30
  version: 3.0.0
31
+ prerelease: false
32
+ type: :runtime
33
33
  - !ruby/object:Gem::Dependency
34
+ name: logstash-patterns-core
35
+ version_requirements: !ruby/object:Gem::Requirement
36
+ requirements:
37
+ - - '>='
38
+ - !ruby/object:Gem::Version
39
+ version: '0'
34
40
  requirement: !ruby/object:Gem::Requirement
35
41
  requirements:
36
42
  - - '>='
37
43
  - !ruby/object:Gem::Version
38
44
  version: '0'
39
- name: logstash-patterns-core
40
45
  prerelease: false
41
46
  type: :runtime
47
+ - !ruby/object:Gem::Dependency
48
+ name: jls-grok
42
49
  version_requirements: !ruby/object:Gem::Requirement
43
50
  requirements:
44
- - - '>='
51
+ - - ~>
45
52
  - !ruby/object:Gem::Version
46
- version: '0'
47
- - !ruby/object:Gem::Dependency
53
+ version: 0.11.1
48
54
  requirement: !ruby/object:Gem::Requirement
49
55
  requirements:
50
56
  - - ~>
51
57
  - !ruby/object:Gem::Version
52
58
  version: 0.11.1
53
- name: jls-grok
54
59
  prerelease: false
55
60
  type: :runtime
61
+ - !ruby/object:Gem::Dependency
62
+ name: logstash-devutils
56
63
  version_requirements: !ruby/object:Gem::Requirement
57
64
  requirements:
58
- - - ~>
65
+ - - '>='
59
66
  - !ruby/object:Gem::Version
60
- version: 0.11.1
61
- - !ruby/object:Gem::Dependency
67
+ version: '0'
62
68
  requirement: !ruby/object:Gem::Requirement
63
69
  requirements:
64
70
  - - '>='
65
71
  - !ruby/object:Gem::Version
66
72
  version: '0'
67
- name: logstash-devutils
68
73
  prerelease: false
69
74
  type: :development
70
- version_requirements: !ruby/object:Gem::Requirement
71
- requirements:
72
- - - '>='
73
- - !ruby/object:Gem::Version
74
- version: '0'
75
75
  description: This gem is a logstash plugin required to be installed on top of the Logstash core pipeline using $LS_HOME/bin/plugin install gemname. This gem is not a stand-alone program
76
76
  email: info@elastic.co
77
77
  executables: []
78
78
  extensions: []
79
79
  extra_rdoc_files: []
80
80
  files:
81
+ - lib/logstash/codecs/identity_map_codec.rb
82
+ - lib/logstash/codecs/multiline.rb
83
+ - spec/codecs/multiline_spec.rb
84
+ - spec/codecs/identity_map_codec_spec.rb
85
+ - spec/supports/helpers.rb
86
+ - logstash-codec-multiline.gemspec
87
+ - README.md
81
88
  - CHANGELOG.md
82
89
  - CONTRIBUTORS
83
90
  - Gemfile
84
91
  - LICENSE
85
92
  - NOTICE.TXT
86
- - README.md
87
- - lib/logstash/codecs/multiline.rb
88
- - logstash-codec-multiline.gemspec
89
- - spec/codecs/multiline_spec.rb
90
- - spec/supports/helpers.rb
91
93
  homepage: http://www.elastic.co/guide/en/logstash/current/index.html
92
94
  licenses:
93
95
  - Apache License (2.0)
@@ -110,10 +112,11 @@ required_rubygems_version: !ruby/object:Gem::Requirement
110
112
  version: '0'
111
113
  requirements: []
112
114
  rubyforge_project:
113
- rubygems_version: 2.4.8
115
+ rubygems_version: 2.1.9
114
116
  signing_key:
115
117
  specification_version: 4
116
118
  summary: The multiline codec will collapse multiline messages and merge them into a single event.
117
119
  test_files:
118
120
  - spec/codecs/multiline_spec.rb
121
+ - spec/codecs/identity_map_codec_spec.rb
119
122
  - spec/supports/helpers.rb