franz 1.3.1 → 1.4.14

Sign up to get free protection for your applications and to get access to all the features.
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