franz 1.3.1 → 1.4.14

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.
data/lib/franz/tail.rb CHANGED
@@ -1,7 +1,7 @@
1
1
  require 'thread'
2
2
  require 'logger'
3
3
 
4
- require 'buftok'
4
+ require 'eventmachine'
5
5
 
6
6
  module Franz
7
7
 
@@ -17,42 +17,23 @@ module Franz
17
17
  @watch_events = opts[:watch_events] || []
18
18
  @tail_events = opts[:tail_events] || []
19
19
 
20
- @eviction_interval = opts[:eviction_interval] || 60
21
- @block_size = opts[:block_size] || 32_768 # 32 KiB
22
- @spread_size = opts[:spread_size] || 98_304 # 96 KiB
23
- @cursors = opts[:cursors] || Hash.new
24
- @logger = opts[:logger] || Logger.new(STDOUT)
25
-
26
- log.debug 'tail: watch_events=%s tail_events=%s' % [
27
- @watch_events, @tail_events
28
- ]
29
-
30
- @buffer = Hash.new { |h, k| h[k] = BufferedTokenizer.new }
31
- @file = Hash.new
32
- @changed = Hash.new
33
- @reading = Hash.new
34
- @stop = false
35
-
36
- @evict_thread = Thread.new do
37
- until @stop
38
- evict
39
- sleep eviction_interval
40
- end
41
- evict true
42
- end
20
+ @line_limit = opts[:line_limit] || 10_240 # 10 KiB
21
+ @block_size = opts[:block_size] || 32_768 # 32 KiB
22
+ @cursors = opts[:cursors] || Hash.new
23
+ @logger = opts[:logger] || Logger.new(STDOUT)
24
+
25
+ @buffer = Hash.new { |h, k| h[k] = BufferedTokenizer.new("\n", @line_limit) }
26
+ @stop = false
43
27
 
44
28
  @tail_thread = Thread.new do
45
- until @stop
46
- if @file.size >= OPEN_FILE_LIMIT
47
- log.debug 'Sleeping until file descriptors become available...'
48
- sleep 5
49
- else
50
- handle(watch_events.shift)
51
- end
52
- end
29
+ handle(watch_events.shift) until @stop
53
30
  end
54
31
 
55
- log.debug 'started tail'
32
+ log.debug \
33
+ event: 'tail started',
34
+ watch_events: watch_events,
35
+ tail_events: tail_events,
36
+ block_size: block_size
56
37
  end
57
38
 
58
39
  # Stop the Tail thread. Effectively only once.
@@ -62,9 +43,8 @@ module Franz
62
43
  return state if @stop
63
44
  @stop = true
64
45
  @watch_thread.kill rescue nil
65
- @evict_thread.kill rescue nil
66
46
  @tail_thread.kill rescue nil
67
- log.debug 'stopped tail'
47
+ log.debug event: 'tail stopped'
68
48
  return state
69
49
  end
70
50
 
@@ -74,93 +54,51 @@ module Franz
74
54
  end
75
55
 
76
56
  private
77
- attr_reader :watch_events, :tail_events, :eviction_interval, :block_size, :cursors, :file, :buffer, :changed, :reading
57
+ attr_reader :watch_events, :tail_events, :block_size, :cursors, :buffer, :reading
78
58
 
79
59
  def log ; @logger end
80
60
 
81
- def open path
82
- if file.size > OPEN_FILE_LIMIT
83
- log.fatal 'Absolutely too many open files!'
84
- raise Errno::EMFILE
85
- end
86
-
87
- return true unless file[path].nil?
88
- pos = @cursors.include?(path) ? @cursors[path] : 0
89
- begin
90
- file[path] = File.open(path)
91
- file[path].sysseek pos, IO::SEEK_SET
92
- @cursors[path] = pos
93
- @changed[path] = Time.now.to_i
94
- rescue Errno::EMFILE
95
- log.debug 'skipping: path=%s (too many open files)' % path.inspect
96
- return false
97
- rescue Errno::ENOENT
98
- log.debug 'skipping: path=%s (file does not exist)' % path.inspect
99
- return false
100
- end
101
- log.trace 'opened: path=%s' % path.inspect
102
- return true
103
- end
104
-
105
61
  def read path, size
106
- @reading[path] = true
107
-
108
- bytes_read = 0
62
+ log.trace \
63
+ event: 'read',
64
+ path: path,
65
+ size: size
66
+ @cursors[path] ||= 0
109
67
  loop do
110
- begin
111
- break if file[path].pos >= size
112
- rescue NoMethodError
113
- break unless open(path)
114
- break if file[path].pos >= size
115
- end
68
+ break if @cursors[path] >= size
116
69
 
117
70
  begin
118
- data = file[path].sysread @block_size
71
+ data = IO::read path, @block_size, @cursors[path]
119
72
  buffer[path].extract(data).each do |line|
120
- log.trace 'captured: path=%s line=%s' % [ path, line ]
73
+ size = line.bytesize
74
+ if size > @line_limit
75
+ log.fatal \
76
+ event: 'killed',
77
+ reason: 'line overflow',
78
+ path: path,
79
+ size: size,
80
+ limit: @line_limit,
81
+ pid: $$
82
+ exit(2)
83
+ end
121
84
  tail_events.push path: path, line: line
122
85
  end
123
- rescue EOFError, Errno::ENOENT
86
+ @cursors[path] += data.bytesize
87
+ rescue EOFError, Errno::ENOENT, NoMethodError
124
88
  # we're done here
125
89
  end
126
-
127
- last_pos = @cursors[path]
128
- @cursors[path] = file[path].pos
129
- bytes_read += @cursors[path] - last_pos
130
90
  end
131
-
132
- log.trace 'read: path=%s size=%s cursor=%s' % [
133
- path.inspect, size.inspect, @cursors[path].inspect
134
- ]
135
-
136
- @changed[path] = Time.now.to_i
137
- @reading.delete path
138
91
  end
139
92
 
140
93
  def close path
141
- @reading[path] = true # prevent evict from interrupting
142
- file.delete(path).close if file.include? path
143
- @cursors.delete(path)
144
- @changed.delete(path)
145
- @reading.delete(path)
146
- log.debug 'closed: path=%s' % path.inspect
147
- end
148
-
149
- def evict force=false
150
- cutoff = Time.now.to_i - eviction_interval
151
- file.keys.each do |path|
152
- unless force
153
- next if @reading[path]
154
- next unless @changed[path] < cutoff
155
- next unless file.include? path
156
- end
157
- file.delete(path).close
158
- log.debug 'evicted: path=%s' % path.inspect
159
- end
94
+ log.trace event: 'close', path: path
95
+ @cursors[path] = 0
160
96
  end
161
97
 
162
98
  def handle event
163
- log.trace 'handle: event=%s' % event.inspect
99
+ log.trace \
100
+ event: 'handle',
101
+ raw: event
164
102
  case event[:name]
165
103
  when :created
166
104
  when :replaced
@@ -176,6 +114,7 @@ module Franz
176
114
  else
177
115
  raise 'invalid event'
178
116
  end
117
+ return event[:path]
179
118
  end
180
119
  end
181
120
  end
data/lib/franz/watch.rb CHANGED
@@ -1,3 +1,5 @@
1
+ require 'set'
2
+
1
3
  require 'logger'
2
4
 
3
5
  module Franz
@@ -21,35 +23,57 @@ module Franz
21
23
  @deletions = opts[:deletions] || []
22
24
  @watch_events = opts[:watch_events] || []
23
25
 
26
+ # Check if files older than STALE_INTERVAL have been updated every
27
+ # SKIP_INTERVAL seconds. Useful if you've got tons of old files.
28
+ @stale_interval = opts[:stale_interval] || 900 # 15 minutes
29
+ @skip_interval = opts[:skip_interval] || 120 # 2 minutes
30
+
31
+ @play_catchup = opts[:play_catchup?].nil? ? true : opts[:play_catchup?]
24
32
  @watch_interval = opts[:watch_interval] || 10
25
33
  @stats = opts[:stats] || Hash.new
26
34
  @logger = opts[:logger] || Logger.new(STDOUT)
27
35
 
28
- # Need to resend old events to make sure Tail catches up
29
- stats.each do |path, old_stat|
30
- watch_events.push name: :appended, path: path, size: old_stat[:size]
36
+ @num_skipped = 0
37
+
38
+ # Make sure we're up-to-date by rewinding our old stats to our cursors
39
+ if @play_catchup
40
+ log.debug event: 'play catchup'
41
+ stats.keys.each do |path|
42
+ stats[path][:size] = opts[:full_state][path][:cursor] || 0
43
+ end
31
44
  end
32
45
 
33
46
  @stop = false
34
-
35
- log.debug 'watch: discoveries=%s deletions=%s watch_events=%s' % [
36
- @discoveries, @deletions, @watch_events
37
- ]
38
-
39
47
  @thread = Thread.new do
48
+ stale_updated = Time.now - @stale_interval
49
+
40
50
  until @stop
51
+ started = Time.now
41
52
  until discoveries.empty?
42
53
  @stats[discoveries.shift] = nil
43
54
  end
44
- watch.each do |deleted|
55
+
56
+ skip_stale = true
57
+ if stale_updated < started - @skip_interval
58
+ skip_stale = false
59
+ stale_updated = Time.now
60
+ end
61
+
62
+ watch(skip_stale).each do |deleted|
45
63
  @stats.delete deleted
46
64
  deletions.push deleted
47
65
  end
66
+
48
67
  sleep watch_interval
49
68
  end
50
69
  end
51
70
 
52
- log.debug 'started watch'
71
+ log.debug \
72
+ event: 'watch started',
73
+ discoveries: discoveries,
74
+ deletions: deletions,
75
+ watch_events: watch_events,
76
+ watch_interval: watch_interval
53
77
  end
54
78
 
55
79
  # Stop the Watch thread. Effectively only once.
@@ -59,7 +83,7 @@ module Franz
59
83
  return state if @stop
60
84
  @stop = true
61
85
  @thread.kill
62
- log.debug 'stopped watch'
86
+ log.debug event: 'watch stopped'
63
87
  return state
64
88
  end
65
89
 
@@ -74,21 +98,38 @@ module Franz
74
98
  def log ; @logger end
75
99
 
76
100
  def enqueue name, path, size=nil
77
- log.trace 'enqueue: name=%s path=%s size=%s' % [
78
- name.inspect, path.inspect, size.inspect
79
- ]
101
+ log.trace \
102
+ event: 'enqueue',
103
+ name: name,
104
+ path: path,
105
+ size: size
80
106
  watch_events.push name: name, path: path, size: size
81
107
  end
82
108
 
83
- def watch
109
+ def watch skip_stale=true
110
+ log.debug \
111
+ event: 'watch',
112
+ skip_stale: skip_stale
84
113
  deleted = []
114
+ skip_past = Time.now - @stale_interval
115
+
85
116
  stats.keys.each do |path|
86
- old_stat = stats[path]
87
- stat = stat_for path
117
+ # Hacks for logs we've removed
118
+ next if File.basename(path) =~ /^rtpstat/
119
+ next if File.basename(path) == 'zuora.log'
120
+
121
+ old_stat = stats[path]
122
+
123
+ next if skip_stale \
124
+ && old_stat \
125
+ && old_stat[:mtime] \
126
+ && old_stat[:mtime] < skip_past
127
+
128
+ stat = stat_for path
88
129
  stats[path] = stat
89
130
 
90
131
  if file_created? old_stat, stat
91
- enqueue :created, path
132
+ # enqueue :created, path
92
133
  elsif file_deleted? old_stat, stat
93
134
  enqueue :deleted, path
94
135
  deleted << path
@@ -119,7 +160,8 @@ module Franz
119
160
  maj: stat.dev_major,
120
161
  min: stat.dev_minor
121
162
  },
122
- size: stat.size
163
+ size: stat.size,
164
+ mtime: stat.mtime
123
165
  }
124
166
  rescue Errno::ENOENT
125
167
  nil
data/lib/franz.rb CHANGED
@@ -1,6 +1,3 @@
1
- open_file_limit = Process.getrlimit(:NOFILE).first
2
- OPEN_FILE_LIMIT = open_file_limit <= 0 ? 256 : open_file_limit
3
-
4
1
  require_relative 'franz/agg'
5
2
  require_relative 'franz/config'
6
3
  require_relative 'franz/discover'
@@ -33,7 +33,7 @@ class TestFranzAgg < MiniTest::Test
33
33
  tmp.write sample
34
34
  tmp.flush
35
35
  tmp.close
36
- start_agg
36
+ start_agg multiline: /^multiline/
37
37
  sleep 3
38
38
  seqs = stop_agg
39
39
  path = realpath tmp.path
@@ -42,6 +42,37 @@ class TestFranzAgg < MiniTest::Test
42
42
  assert seqs[path] == 1 # should be one line
43
43
  end
44
44
 
45
+ def test_handles_singular_drop
46
+ sample = "drop this\nbut not this\n"
47
+ tmp = tempfile %w[ test1 .log ]
48
+ tmp.write sample
49
+ tmp.flush
50
+ tmp.close
51
+ start_agg drop: /^drop/
52
+ sleep 3
53
+ seqs = stop_agg
54
+ path = realpath tmp.path
55
+ assert seqs.include?(path)
56
+ assert_equal sample.lines.last.strip, @agg_events.shift[:message]
57
+ assert seqs[path] == 1 # should be one line
58
+ end
59
+
60
+ def test_handles_plural_drop
61
+ sample = "drop this\nbut not this\nignore this too\nreally\n"
62
+ tmp = tempfile %w[ test1 .log ]
63
+ tmp.write sample
64
+ tmp.flush
65
+ tmp.close
66
+ start_agg drop: [ /^drop/, /^ignore/ ]
67
+ sleep 5
68
+ seqs = stop_agg
69
+ path = realpath tmp.path
70
+ assert seqs.include?(path)
71
+ assert_equal sample.lines[1].strip, @agg_events.shift[:message]
72
+ assert_equal sample.lines[3].strip, @agg_events.shift[:message]
73
+ assert seqs[path] == 2 # should be two lines
74
+ end
75
+
45
76
  private
46
77
  def tempfile prefix=nil
47
78
  Tempfile.new prefix, @tmpdir
@@ -51,13 +82,12 @@ private
51
82
  Pathname.new(path).realpath.to_s
52
83
  end
53
84
 
54
- def start_agg opts={}
85
+ def start_agg config, opts={}
55
86
  configs = [{
56
87
  type: :test,
57
- multiline: /^multiline/,
58
88
  includes: [ "#{@tmpdir}/*.log", "#{realpath @tmpdir}/*.log" ],
59
89
  excludes: [ "#{@tmpdir}/exclude*" ]
60
- }]
90
+ }.merge(config)]
61
91
 
62
92
  @discover = Franz::Discover.new({
63
93
  discover_interval: 1,
@@ -24,23 +24,23 @@ class TestFranzDiscover < MiniTest::Test
24
24
  end
25
25
 
26
26
  def test_discovers_existing_file
27
- tmp = tempfile %w[ test .log ]
27
+ tmp = tempfile %w[ test1 .log ]
28
28
  start_discovery known: []
29
- sleep 0.001 # Time to discover
29
+ sleep 2 # Time to discover
30
30
  known = stop_discovery
31
31
  assert known.include?(tmp.path)
32
32
  end
33
33
 
34
34
  def test_discovers_new_file
35
35
  start_discovery known: []
36
- tmp = tempfile %w[ test .log ]
37
- sleep 1 # Time to discover
36
+ tmp = tempfile %w[ test2 .log ]
37
+ sleep 3 # Time to discover
38
38
  known = stop_discovery
39
39
  assert known.include?(tmp.path)
40
40
  end
41
41
 
42
42
  def test_deletes_deleted_file
43
- tmp = tempfile %w[ test .log ]
43
+ tmp = tempfile %w[ test3 .log ]
44
44
  start_discovery known: []
45
45
  # at this point, we know Discover has already picked up tmp
46
46
  delete tmp.path
@@ -50,11 +50,11 @@ class TestFranzDiscover < MiniTest::Test
50
50
  end
51
51
 
52
52
  def test_deletes_unknown_file
53
- tmp = tempfile %w[ test .log ]
53
+ tmp = tempfile %w[ test4 .log ]
54
54
  delete tmp.path
55
55
  # tmp never exists as far as Discover is aware
56
56
  start_discovery known: []
57
- sleep 0.001
57
+ sleep 2
58
58
  known = stop_discovery
59
59
  assert !known.include?(tmp.path)
60
60
  end
@@ -25,6 +25,27 @@ class TestFranzTail < MiniTest::Test
25
25
  FileUtils.rm_rf @tmpdir
26
26
  end
27
27
 
28
+ def test_handles_reading_after_deletion
29
+ sample = "Hello, world!\n"
30
+ start_tail
31
+ tmp = tempfile %w[ test4 .log ]
32
+ path = tmp.path
33
+ tmp.write sample
34
+ tmp.flush
35
+ tmp.close
36
+ sleep 1
37
+ FileUtils.rm_rf path
38
+ sleep 2
39
+ File.open(path, 'w') do |f|
40
+ f.write sample
41
+ f.flush
42
+ end
43
+ sleep 4
44
+ cursors = stop_tail
45
+ assert cursors.include?(tmp.path)
46
+ assert cursors[tmp.path] == sample.length
47
+ end
48
+
28
49
  def test_handles_existing_file
29
50
  sample = "Hello, world!\n"
30
51
  tmp = tempfile %w[ test1 .log ]
@@ -57,7 +78,7 @@ class TestFranzTail < MiniTest::Test
57
78
  eviction_interval = 2
58
79
  start_tail eviction_interval: eviction_interval
59
80
  sleep 0
60
- tmp = tempfile %w[ test2 .log ]
81
+ tmp = tempfile %w[ test3 .log ]
61
82
  tmp.write sample
62
83
  tmp.flush
63
84
  sleep eviction_interval / 2
@@ -70,29 +91,6 @@ class TestFranzTail < MiniTest::Test
70
91
  assert cursors[tmp.path] == sample.length * 2
71
92
  end
72
93
 
73
- def test_handles_reading_after_deletion
74
- sample = "Hello, world!\n"
75
- eviction_interval = 2
76
- start_tail eviction_interval: eviction_interval
77
- sleep 0
78
- tmp = tempfile %w[ test2 .log ]
79
- path = tmp.path
80
- tmp.write sample
81
- tmp.flush
82
- tmp.close
83
- sleep eviction_interval / 2
84
- FileUtils.rm_rf path
85
- sleep eviction_interval
86
- File.open(path, 'w') do |f|
87
- f.write sample
88
- f.flush
89
- end
90
- sleep eviction_interval * 2
91
- cursors = stop_tail
92
- assert cursors.include?(tmp.path)
93
- assert cursors[tmp.path] == sample.length
94
- end
95
-
96
94
  private
97
95
  def tempfile prefix=nil
98
96
  Tempfile.new prefix, @tmpdir
@@ -25,7 +25,7 @@ class TestFranzWatch < MiniTest::Test
25
25
  end
26
26
 
27
27
  def test_handles_existing_file
28
- tmp = tempfile %w[ test .log ]
28
+ tmp = tempfile %w[ test1 .log ]
29
29
  start_watch
30
30
  sleep 2
31
31
  stats = stop_watch
@@ -35,7 +35,7 @@ class TestFranzWatch < MiniTest::Test
35
35
 
36
36
  def test_handles_existing_file_with_content
37
37
  content = "Hello, world!\n"
38
- tmp = tempfile %w[ test .log ]
38
+ tmp = tempfile %w[ test2 .log ]
39
39
  tmp.write content
40
40
  tmp.flush
41
41
  start_watch
@@ -47,7 +47,7 @@ class TestFranzWatch < MiniTest::Test
47
47
 
48
48
  def test_handles_new_file
49
49
  start_watch
50
- tmp = tempfile %w[ test .log ]
50
+ tmp = tempfile %w[ test3 .log ]
51
51
  sleep 3
52
52
  stats = stop_watch
53
53
  assert stats.include?(tmp.path)
@@ -57,7 +57,7 @@ class TestFranzWatch < MiniTest::Test
57
57
  def test_handles_new_file_with_content
58
58
  start_watch
59
59
  content = "Hello, world!\n"
60
- tmp = tempfile %w[ test .log ]
60
+ tmp = tempfile %w[ test4 .log ]
61
61
  tmp.write content
62
62
  tmp.flush
63
63
  sleep 3
@@ -70,7 +70,7 @@ class TestFranzWatch < MiniTest::Test
70
70
  long_content = "Hello, world!\n"
71
71
  short_content = "Bye!\n"
72
72
 
73
- tmp = tempfile %w[ test .log ]
73
+ tmp = tempfile %w[ test5 .log ]
74
74
  tmp.write long_content
75
75
  tmp.flush
76
76
 
@@ -91,12 +91,12 @@ class TestFranzWatch < MiniTest::Test
91
91
  content1 = "Hello, world!\n"
92
92
  content2 = "Bye!\n"
93
93
 
94
- tmp1 = tempfile %w[ test .log ]
94
+ tmp1 = tempfile %w[ test6 .log ]
95
95
  tmp1.write content1
96
96
  tmp1.flush
97
97
  tmp1.close
98
98
 
99
- tmp2 = tempfile %w[ exclude .log ]
99
+ tmp2 = tempfile %w[ exclude6 .log ]
100
100
  tmp2.write content2
101
101
  tmp2.flush
102
102
  tmp2.close
@@ -0,0 +1,126 @@
1
+ require 'thread'
2
+ require 'tmpdir'
3
+ require 'tempfile'
4
+ require 'fileutils'
5
+ require 'pathname'
6
+ require 'minitest/autorun'
7
+
8
+ require 'deep_merge'
9
+
10
+ require_relative '../lib/franz'
11
+
12
+ Thread.abort_on_exception = true
13
+
14
+ class TestPerformance < MiniTest::Test
15
+ def setup
16
+ @ulimit = Process.getrlimit(:NOFILE).first
17
+
18
+ @discover_interval = 2
19
+ @watch_interval = 2
20
+ @eviction_interval = 2
21
+ @flush_interval = 2
22
+
23
+ @tmpdir = File.join(Dir.pwd, 'tmp')
24
+ @discoveries = Queue.new
25
+ @deletions = Queue.new
26
+ @watch_events = Queue.new
27
+ @tail_events = Queue.new
28
+ @agg_events = Queue.new
29
+ @logger = Logger.new STDERR
30
+ @logger.level = Logger::WARN
31
+
32
+ FileUtils.rm_rf @tmpdir
33
+ FileUtils.mkdir_p @tmpdir
34
+ end
35
+
36
+ def teardown
37
+ # nop
38
+ end
39
+
40
+ def test_handles_too_many_files_for_ulimit
41
+ sample = "Why, hello there, World! How lovely to see you this morning."
42
+ num_files = @ulimit * 2
43
+ num_lines_per_file = 100
44
+
45
+ paths = []
46
+ num_files.times do |i|
47
+ path = File.join(@tmpdir, "test.#{i}.log")
48
+ File.open(path, 'w') do |f|
49
+ num_lines_per_file.times do
50
+ f.puts sample
51
+ end
52
+ end
53
+ paths << path
54
+ end
55
+
56
+ num_events = num_files * num_lines_per_file
57
+
58
+ start_agg
59
+ started = Time.now
60
+ until @agg_events.size == num_events
61
+ sleep 1
62
+ end
63
+ seqs = stop_agg
64
+ elapsed = Time.now - started
65
+
66
+ @logger.fatal('%ds elapsed' % elapsed)
67
+ @logger.fatal('%d events' % num_events)
68
+ @logger.fatal('%f events/s' % ( (1.0 * num_events) / (1.0 * elapsed) ))
69
+ assert_equal(paths.size, seqs.keys.size)
70
+ assert_equal(paths.size * num_lines_per_file, @agg_events.size)
71
+ end
72
+
73
+
74
+
75
+ private
76
+ def tempfile prefix=nil
77
+ Tempfile.new prefix, @tmpdir
78
+ end
79
+
80
+ def realpath path
81
+ Pathname.new(path).realpath.to_s.gsub(/^\/private/, '')
82
+ end
83
+
84
+ def start_agg opts={}
85
+ configs = [{
86
+ type: :test,
87
+ includes: [ "#{@tmpdir}/*.log" ],
88
+ excludes: [ "#{@tmpdir}/exclude*" ]
89
+ }]
90
+
91
+ @discover = Franz::Discover.new({
92
+ discover_interval: @discover_interval,
93
+ discoveries: @discoveries,
94
+ deletions: @deletions,
95
+ logger: @logger,
96
+ configs: configs
97
+ }.deep_merge!(opts))
98
+
99
+ @watch = Franz::Watch.new({
100
+ watch_interval: @watch_interval,
101
+ watch_events: @watch_events,
102
+ discoveries: @discoveries,
103
+ deletions: @deletions,
104
+ logger: @logger
105
+ }.deep_merge!(opts))
106
+
107
+ @tail = Franz::Tail.new({
108
+ eviction_interval: @eviction_interval,
109
+ watch_events: @watch_events,
110
+ tail_events: @tail_events,
111
+ logger: @logger
112
+ }.deep_merge!(opts))
113
+
114
+ @agg = Franz::Agg.new({
115
+ configs: configs,
116
+ flush_interval: @flush_interval,
117
+ tail_events: @tail_events,
118
+ agg_events: @agg_events,
119
+ logger: @logger
120
+ }.deep_merge!(opts))
121
+ end
122
+
123
+ def stop_agg
124
+ @agg.stop
125
+ end
126
+ end