|
1 | 1 | require 'logger'
|
2 | 2 | require 'thread'
|
3 |
| -require 'fileutils' |
4 |
| -require 'securerandom' |
| 3 | +require 'csv' |
5 | 4 |
|
6 | 5 | module LogStash
|
7 | 6 | module Outputs
|
8 | 7 | class CustomSizeBasedBuffer
|
9 |
| - def initialize(max_size_mb, max_interval, &flush_callback) |
| 8 | + def initialize(max_size_mb = 10, max_interval = 10, max_retries = 3, failed_items_path = nil, &flush_callback) |
10 | 9 | @buffer_config = {
|
11 | 10 | max_size: max_size_mb * 1024 * 1024, # Convert MB to bytes
|
12 | 11 | max_interval: max_interval,
|
13 |
| - buffer_dir: './tmp/buffer_storage/', |
| 12 | + max_retries: max_retries, |
| 13 | + failed_items_path: failed_items_path, |
14 | 14 | logger: Logger.new(STDOUT)
|
15 | 15 | }
|
16 | 16 | @buffer_state = {
|
17 | 17 | pending_items: [],
|
18 | 18 | pending_size: 0,
|
19 | 19 | last_flush: Time.now.to_i,
|
20 |
| - timer: nil, |
21 |
| - network_down: false |
22 |
| - } |
23 |
| - @flush_callback = flush_callback |
24 |
| - @shutdown = false |
25 |
| - @pending_mutex = Mutex.new |
26 |
| - @flush_mutex = Mutex.new |
27 |
| - load_buffer_from_files |
28 |
| - @buffer_config[:logger].info("CustomSizeBasedBuffer initialized with max_size: #{max_size_mb} MB, max_interval: #{max_interval} seconds") |
29 |
| - |
30 |
| - # Start the timer thread after a delay to ensure initializations are completed |
31 |
| - Thread.new do |
32 |
| - sleep(10) |
33 |
| - @buffer_state[:timer] = Thread.new do |
| 20 | + timer: Thread.new do |
34 | 21 | loop do
|
35 | 22 | sleep(@buffer_config[:max_interval])
|
36 | 23 | buffer_flush(force: true)
|
37 | 24 | end
|
38 | 25 | end
|
39 |
| - end |
| 26 | + } |
| 27 | + @flush_callback = flush_callback |
| 28 | + @pending_mutex = Mutex.new |
| 29 | + @flush_mutex = Mutex.new |
| 30 | + @buffer_config[:logger].info("CustomSizeBasedBuffer initialized with max_size: #{max_size_mb} MB, max_interval: #{max_interval} seconds, max_retries: #{max_retries}, failed_items_path: #{failed_items_path}") |
40 | 31 | end
|
41 | 32 |
|
42 |
| - def <<(event) |
43 |
| - while buffer_full? do |
44 |
| - sleep 0.1 |
45 |
| - end |
| 33 | + def <<(event) |
| 34 | + while buffer_full? do |
| 35 | + sleep 0.1 |
| 36 | + end |
46 | 37 |
|
47 | 38 | @pending_mutex.synchronize do
|
48 | 39 | @buffer_state[:pending_items] << event
|
49 | 40 | @buffer_state[:pending_size] += event.bytesize
|
50 | 41 | end
|
| 42 | + |
| 43 | + # Trigger a flush if the buffer size exceeds the maximum size |
| 44 | + if buffer_full? |
| 45 | + buffer_flush(force: true) |
| 46 | + end |
51 | 47 | end
|
52 | 48 |
|
53 | 49 | def shutdown
|
54 | 50 | @buffer_config[:logger].info("Shutting down buffer")
|
55 |
| - @shutdown = true |
56 | 51 | @buffer_state[:timer].kill
|
57 | 52 | buffer_flush(final: true)
|
58 |
| - clear_buffer_files |
59 | 53 | end
|
60 | 54 |
|
61 |
| - private |
| 55 | + private |
62 | 56 |
|
63 |
| - def buffer_full? |
64 |
| - @pending_mutex.synchronize do |
65 |
| - @buffer_state[:pending_size] >= @buffer_config[:max_size] |
66 |
| - end |
67 |
| - end |
68 |
| - |
69 |
| - def buffer_flush(options = {}) |
70 |
| - force = options[:force] || options[:final] |
71 |
| - final = options[:final] |
| 57 | + def buffer_full? |
| 58 | + @pending_mutex.synchronize do |
| 59 | + @buffer_state[:pending_size] >= @buffer_config[:max_size] |
| 60 | + end |
| 61 | + end |
72 | 62 |
|
73 |
| - if final |
74 |
| - @flush_mutex.lock |
75 |
| - elsif !@flush_mutex.try_lock |
76 |
| - return 0 |
77 |
| - end |
| 63 | + def buffer_flush(options = {}) |
| 64 | + force = options[:force] || options[:final] |
| 65 | + final = options[:final] |
78 | 66 |
|
79 |
| - items_flushed = 0 |
| 67 | + if final |
| 68 | + @flush_mutex.lock |
| 69 | + elsif !@flush_mutex.try_lock |
| 70 | + return 0 |
| 71 | + end |
80 | 72 |
|
81 | 73 | begin
|
82 | 74 | outgoing_items = []
|
83 | 75 | outgoing_size = 0
|
84 | 76 |
|
85 | 77 | @pending_mutex.synchronize do
|
86 | 78 | return 0 if @buffer_state[:pending_size] == 0
|
87 |
| - |
88 |
| - time_since_last_flush = Time.now.to_i - @buffer_state[:last_flush] |
| 79 | + time_since_last_flush = Time.now.to_i - @buffer_state[:last_flush] |
89 | 80 |
|
90 | 81 | if !force && @buffer_state[:pending_size] < @buffer_config[:max_size] && time_since_last_flush < @buffer_config[:max_interval]
|
91 | 82 | return 0
|
92 | 83 | end
|
93 | 84 |
|
94 | 85 | if force
|
95 |
| - @buffer_config[:logger].info("Time-based flush triggered after #{@buffer_config[:max_interval]} seconds") |
96 |
| - elsif @buffer_state[:pending_size] >= @buffer_config[:max_size] |
97 |
| - @buffer_config[:logger].info("Size-based flush triggered at #{@buffer_state[:pending_size]} bytes was reached") |
98 |
| - else |
99 |
| - @buffer_config[:logger].info("Flush triggered without specific condition") |
| 86 | + if time_since_last_flush >= @buffer_config[:max_interval] |
| 87 | + @buffer_config[:logger].info("Time-based flush triggered after #{@buffer_config[:max_interval]} seconds") |
| 88 | + else |
| 89 | + @buffer_config[:logger].info("Size-based flush triggered when #{@buffer_state[:pending_size]} bytes was reached") |
| 90 | + end |
100 | 91 | end
|
101 | 92 |
|
102 | 93 | outgoing_items = @buffer_state[:pending_items].dup
|
103 | 94 | outgoing_size = @buffer_state[:pending_size]
|
104 |
| - @buffer_state[:pending_items] = [] |
105 |
| - @buffer_state[:pending_size] = 0 |
| 95 | + buffer_initialize |
106 | 96 | end
|
107 | 97 |
|
| 98 | + retries = 0 |
108 | 99 | begin
|
| 100 | + @buffer_config[:logger].info("Flushing: #{outgoing_items.size} items and #{outgoing_size} bytes to the network") |
109 | 101 | @flush_callback.call(outgoing_items) # Pass the list of events to the callback
|
110 |
| - @buffer_state[:network_down] = false # Reset network status after successful flush |
111 |
| - flush_buffer_files # Flush buffer files if any exist |
| 102 | + @buffer_state[:last_flush] = Time.now.to_i |
| 103 | + @buffer_config[:logger].info("Flush completed. Flushed #{outgoing_items.size} events, #{outgoing_size} bytes") |
112 | 104 | rescue => e
|
113 |
| - @buffer_config[:logger].error("Flush failed: #{e.message}") |
114 |
| - @buffer_state[:network_down] = true |
115 |
| - save_buffer_to_file(outgoing_items) |
| 105 | + retries += 1 |
| 106 | + if retries <= @buffer_config[:max_retries] |
| 107 | + @buffer_config[:logger].error("Flush failed: #{e.message}. \nRetrying (#{retries}/#{@buffer_config[:max_retries]})...") |
| 108 | + sleep 1 |
| 109 | + retry |
| 110 | + else |
| 111 | + @buffer_config[:logger].error("Max retries reached. Failed to flush #{outgoing_items.size} items and #{outgoing_size} bytes") |
| 112 | + handle_failed_flush(outgoing_items, e.message) |
| 113 | + end |
116 | 114 | end
|
117 | 115 |
|
118 |
| - @buffer_state[:last_flush] = Time.now.to_i |
119 |
| - @buffer_config[:logger].info("Flush completed. Flushed #{outgoing_items.size} events, #{outgoing_size} bytes") |
120 |
| - |
121 |
| - items_flushed = outgoing_items.size |
122 | 116 | ensure
|
123 | 117 | @flush_mutex.unlock
|
124 | 118 | end
|
125 |
| - |
126 |
| - items_flushed |
127 |
| - end |
128 |
| - |
129 |
| - def save_buffer_to_file(events) |
130 |
| - buffer_state_copy = { |
131 |
| - pending_items: events, |
132 |
| - pending_size: events.sum(&:bytesize) |
133 |
| - } |
134 |
| - |
135 |
| - ::FileUtils.mkdir_p(@buffer_config[:buffer_dir]) # Ensure directory exists |
136 |
| - file_path = ::File.join(@buffer_config[:buffer_dir], "buffer_state_#{Time.now.to_i}_#{SecureRandom.uuid}.log") |
137 |
| - ::File.open(file_path, 'w') do |file| |
138 |
| - file.write(Marshal.dump(buffer_state_copy)) |
139 |
| - end |
140 |
| - @buffer_config[:logger].info("Saved buffer state to file: #{file_path}") |
141 | 119 | end
|
142 | 120 |
|
143 |
| - def load_buffer_from_files |
144 |
| - Dir.glob(::File.join(@buffer_config[:buffer_dir], 'buffer_state_*.log')).each do |file_path| |
| 121 | + def handle_failed_flush(items, error_message) |
| 122 | + if @buffer_config[:failed_items_path] |
145 | 123 | begin
|
146 |
| - buffer_state = Marshal.load(::File.read(file_path)) |
147 |
| - @buffer_state[:pending_items].concat(buffer_state[:pending_items]) |
148 |
| - @buffer_state[:pending_size] += buffer_state[:pending_size] |
149 |
| - ::File.delete(file_path) |
150 |
| - rescue => e |
151 |
| - @buffer_config[:logger].error("Failed to load buffer from file: #{e.message}") |
152 |
| - end |
153 |
| - end |
154 |
| - @buffer_config[:logger].info("Loaded buffer state from files") |
155 |
| - end |
156 |
| - |
157 |
| - def flush_buffer_files |
158 |
| - Dir.glob(::File.join(@buffer_config[:buffer_dir], 'buffer_state_*.log')).each do |file_path| |
159 |
| - begin |
160 |
| - buffer_state = Marshal.load(::File.read(file_path)) |
161 |
| - @buffer_config[:logger].info("Flushed from file: #{file_path}") |
162 |
| - @flush_callback.call(buffer_state[:pending_items]) |
163 |
| - ::File.delete(file_path) |
164 |
| - @buffer_config[:logger].info("Flushed and deleted buffer state file: #{file_path}") |
| 124 | + ::File.open(@buffer_config[:failed_items_path], 'a') do |file| |
| 125 | + items.each do |item| |
| 126 | + file.puts(item) |
| 127 | + end |
| 128 | + end |
| 129 | + @buffer_config[:logger].info("Failed items stored in #{@buffer_config[:failed_items_path]}") |
165 | 130 | rescue => e
|
166 |
| - @buffer_config[:logger].error("Failed to flush buffer state file: #{e.message}") |
167 |
| - break |
| 131 | + @buffer_config[:logger].error("Failed to store items: #{e.message}") |
168 | 132 | end
|
| 133 | + else |
| 134 | + @buffer_config[:logger].warn("No failed_items_path configured. Data loss may occur.") |
169 | 135 | end
|
170 | 136 | end
|
171 | 137 |
|
172 |
| - def clear_buffer_files |
173 |
| - Dir.glob(::File.join(@buffer_config[:buffer_dir], 'buffer_state_*.log')).each do |file_path| |
174 |
| - ::File.delete(file_path) |
175 |
| - end |
176 |
| - @buffer_config[:logger].info("Cleared all buffer state files") |
| 138 | + def buffer_initialize |
| 139 | + @buffer_state[:pending_items] = [] |
| 140 | + @buffer_state[:pending_size] = 0 |
177 | 141 | end
|
178 | 142 | end
|
179 | 143 | end
|
|
0 commit comments