timber 2.0.24 → 2.1.0.rc1

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.
Files changed (97) hide show
  1. checksums.yaml +4 -4
  2. data/.travis.yml +2 -2
  3. data/CHANGELOG +3 -0
  4. data/README.md +314 -59
  5. data/bin/timber +11 -2
  6. data/lib/timber.rb +2 -7
  7. data/lib/timber/cli.rb +16 -28
  8. data/lib/timber/cli/api.rb +80 -14
  9. data/lib/timber/cli/api/application.rb +30 -0
  10. data/lib/timber/cli/config_file.rb +66 -0
  11. data/lib/timber/cli/file_helper.rb +43 -0
  12. data/lib/timber/cli/installer.rb +58 -0
  13. data/lib/timber/cli/installers.rb +37 -0
  14. data/lib/timber/cli/installers/other.rb +47 -0
  15. data/lib/timber/cli/installers/rails.rb +255 -0
  16. data/lib/timber/cli/installers/root.rb +189 -0
  17. data/lib/timber/cli/io.rb +97 -0
  18. data/lib/timber/cli/io/ansi.rb +22 -0
  19. data/lib/timber/cli/io/messages.rb +213 -0
  20. data/lib/timber/cli/os_helper.rb +53 -0
  21. data/lib/timber/config.rb +97 -43
  22. data/lib/timber/config/integrations.rb +63 -0
  23. data/lib/timber/config/integrations/rack.rb +74 -0
  24. data/lib/timber/context.rb +13 -10
  25. data/lib/timber/contexts.rb +1 -0
  26. data/lib/timber/contexts/custom.rb +16 -3
  27. data/lib/timber/contexts/http.rb +10 -3
  28. data/lib/timber/contexts/organization.rb +4 -0
  29. data/lib/timber/contexts/release.rb +46 -0
  30. data/lib/timber/contexts/runtime.rb +7 -1
  31. data/lib/timber/contexts/session.rb +8 -1
  32. data/lib/timber/contexts/system.rb +5 -1
  33. data/lib/timber/contexts/user.rb +9 -2
  34. data/lib/timber/current_context.rb +43 -11
  35. data/lib/timber/events/controller_call.rb +4 -0
  36. data/lib/timber/events/custom.rb +13 -5
  37. data/lib/timber/events/exception.rb +4 -0
  38. data/lib/timber/events/http_client_request.rb +4 -0
  39. data/lib/timber/events/http_client_response.rb +4 -0
  40. data/lib/timber/events/http_server_request.rb +5 -0
  41. data/lib/timber/events/http_server_response.rb +15 -3
  42. data/lib/timber/events/sql_query.rb +3 -0
  43. data/lib/timber/events/template_render.rb +3 -0
  44. data/lib/timber/integration.rb +40 -0
  45. data/lib/timber/integrations.rb +21 -14
  46. data/lib/timber/integrations/action_controller.rb +18 -0
  47. data/lib/timber/integrations/action_controller/log_subscriber.rb +2 -0
  48. data/lib/timber/integrations/action_controller/log_subscriber/timber_log_subscriber.rb +6 -0
  49. data/lib/timber/integrations/action_dispatch.rb +23 -0
  50. data/lib/timber/integrations/action_dispatch/debug_exceptions.rb +2 -0
  51. data/lib/timber/integrations/action_view.rb +18 -0
  52. data/lib/timber/integrations/action_view/log_subscriber.rb +2 -0
  53. data/lib/timber/integrations/action_view/log_subscriber/timber_log_subscriber.rb +10 -0
  54. data/lib/timber/integrations/active_record.rb +18 -0
  55. data/lib/timber/integrations/active_record/log_subscriber.rb +2 -0
  56. data/lib/timber/integrations/active_record/log_subscriber/timber_log_subscriber.rb +8 -0
  57. data/lib/timber/integrations/rack.rb +12 -2
  58. data/lib/timber/integrations/rack/exception_event.rb +38 -5
  59. data/lib/timber/integrations/rack/http_context.rb +4 -6
  60. data/lib/timber/integrations/rack/http_events.rb +177 -27
  61. data/lib/timber/integrations/rack/middleware.rb +28 -0
  62. data/lib/timber/integrations/rack/session_context.rb +5 -6
  63. data/lib/timber/integrations/rack/user_context.rb +90 -43
  64. data/lib/timber/integrations/rails.rb +22 -0
  65. data/lib/timber/integrations/rails/rack_logger.rb +2 -0
  66. data/lib/timber/integrator.rb +18 -3
  67. data/lib/timber/log_devices/http.rb +107 -99
  68. data/lib/timber/log_devices/http/dropping_sized_queue.rb +26 -0
  69. data/lib/timber/log_devices/http/flushable_sized_queue.rb +42 -0
  70. data/lib/timber/log_entry.rb +14 -2
  71. data/lib/timber/logger.rb +51 -36
  72. data/lib/timber/overrides.rb +2 -0
  73. data/lib/timber/overrides/active_support_3_tagged_logging.rb +103 -0
  74. data/lib/timber/overrides/active_support_tagged_logging.rb +53 -90
  75. data/lib/timber/timer.rb +21 -0
  76. data/lib/timber/util/hash.rb +1 -1
  77. data/lib/timber/util/http_event.rb +16 -3
  78. data/lib/timber/version.rb +1 -1
  79. data/spec/support/timber.rb +2 -3
  80. data/spec/timber/cli/installers/rails_spec.rb +160 -0
  81. data/spec/timber/cli/installers/root_spec.rb +100 -0
  82. data/spec/timber/config_spec.rb +28 -0
  83. data/spec/timber/current_context_spec.rb +61 -12
  84. data/spec/timber/events/custom_spec.rb +13 -2
  85. data/spec/timber/events/exception_spec.rb +15 -0
  86. data/spec/timber/events/http_server_request_spec.rb +3 -3
  87. data/spec/timber/integrations/rack/http_events_spec.rb +101 -0
  88. data/spec/timber/log_devices/http_spec.rb +20 -4
  89. data/spec/timber/log_entry_spec.rb +2 -1
  90. data/spec/timber/logger_spec.rb +8 -8
  91. metadata +40 -9
  92. data/benchmarks/rails.rb +0 -122
  93. data/lib/timber/cli/application.rb +0 -28
  94. data/lib/timber/cli/install.rb +0 -196
  95. data/lib/timber/cli/io_helper.rb +0 -65
  96. data/lib/timber/cli/messages.rb +0 -180
  97. data/lib/timber/integrations/active_support/tagged_logging.rb +0 -71
@@ -0,0 +1,22 @@
1
+ require "timber/integration"
2
+ require "timber/integrations/rack/http_events"
3
+ require "timber/integrations/rails/rack_logger"
4
+
5
+ module Timber
6
+ module Integrations
7
+ # Module for holding *all* Rails integrations. This module does *not*
8
+ # extend {Integration} because it's dependent on {Rack::HTTPEvents}. This
9
+ # module simply disables the default HTTP request logging.
10
+ module Rails
11
+ def self.enabled?
12
+ Rack::HTTPEvents.enabled?
13
+ end
14
+
15
+ def self.integrate!
16
+ return false if !enabled?
17
+
18
+ RackLogger.integrate!
19
+ end
20
+ end
21
+ end
22
+ end
@@ -1,3 +1,5 @@
1
+ require "timber/integrator"
2
+
1
3
  module Timber
2
4
  module Integrations
3
5
  module Rails
@@ -1,17 +1,31 @@
1
1
  module Timber
2
- # Base class for `Timber::Integrations::*`.
3
- #
4
- # @private
2
+ # Base class for `Timber::Integrations::*`. Provides a common interface for all integrators.
3
+ # An integrator is a single specific integration into a part of a library. See
4
+ # {Integration} for higher library level integration settings.
5
5
  class Integrator
6
+ # Raised when an integrators requirements are not met. For example, this will be raised
7
+ # in the ActiveRecord integration if ActiveRecord is not available as a dependency in
8
+ # the current application.
6
9
  class RequirementNotMetError < StandardError; end
7
10
 
8
11
  class << self
9
12
  attr_writer :enabled
10
13
 
14
+ # Allows you to enable / disable specific integrations.
15
+ #
16
+ # @note Disabling specific low level integrations should only be needed for edge cases.
17
+ # If you want to disable integration with an entire library, we recommend doing so
18
+ # at a higher level. Ex: `Timber::Integrations::ActiveRecord.enabled = false`.
19
+ #
20
+ # @example
21
+ # Timber::Integrations::ActiveRecord::LogSubscriber.enabled = false
11
22
  def enabled?
12
23
  @enabled != false
13
24
  end
14
25
 
26
+ # Convenience class level method that runs the integrator by instantiating a new
27
+ # object and calling {#integrate!}. It also takes care to look at the if the integrator
28
+ # is enabled, skipping it if not.
15
29
  def integrate!(*args)
16
30
  if !enabled?
17
31
  Config.instance.debug_logger.debug("#{name} integration disabled, skipping") if Config.instance.debug_logger
@@ -28,6 +42,7 @@ module Timber
28
42
  end
29
43
  end
30
44
 
45
+ # Abstract method that each integration must implement.
31
46
  def integrate!
32
47
  raise NotImplementedError.new
33
48
  end
@@ -1,81 +1,38 @@
1
1
  require "base64"
2
+ require "msgpack"
2
3
  require "net/https"
3
4
 
5
+ require "timber/config"
6
+ require "timber/log_devices/http/dropping_sized_queue"
7
+ require "timber/log_devices/http/flushable_sized_queue"
8
+ require "timber/version"
9
+
4
10
  module Timber
5
11
  module LogDevices
6
12
  # A highly efficient log device that buffers and delivers log messages over HTTPS to
7
13
  # the Timber API. It uses batches, keep-alive connections, and msgpack to deliver logs with
8
14
  # high-throughput and little overhead. All log preparation and delivery is done asynchronously
9
- # in a thread as not to block application execution.
15
+ # in a thread as not to block application execution and efficient deliver logs for
16
+ # multi-threaded environments.
10
17
  #
11
18
  # See {#initialize} for options and more details.
12
19
  class HTTP
13
- # @private
14
- class LogMsgQueue
15
- def initialize(max_size)
16
- @lock = Mutex.new
17
- @max_size = max_size
18
- @array = []
19
- end
20
-
21
- def enqueue(msg)
22
- @lock.synchronize do
23
- @array << msg
24
- end
25
- end
26
-
27
- def flush
28
- @lock.synchronize do
29
- old = @array
30
- @array = []
31
- return old
32
- end
33
- end
34
-
35
- def full?
36
- size >= @max_size
37
- end
38
-
39
- def size
40
- @array.size
41
- end
42
- end
43
-
44
- # Works like SizedQueue, but drops message instead of blocking. Pass one of these in
45
- # to {HTTP#intiialize} via the :request_queue option if you'd prefer to drop messages
46
- # in the event of a buffer overflow instead of applying back pressure.
47
- class DroppingSizedQueue < SizedQueue
48
- # Returns true/false depending on whether the queue is full or not
49
- def push(obj)
50
- @mutex.synchronize do
51
- return false unless @que.length < @max
52
-
53
- @que.push obj
54
- begin
55
- t = @waiting.shift
56
- t.wakeup if t
57
- rescue ThreadError
58
- retry
59
- end
60
- return true
61
- end
62
- end
63
- end
64
-
65
- TIMBER_URL = "https://logs.timber.io/frames".freeze
20
+ TIMBER_STAGING_URL = "https://logs-staging.timber.io/frames".freeze
21
+ TIMBER_PRODUCTION_URL = "https://logs.timber.io/frames".freeze
22
+ TIMBER_URL = ENV['TIMBER_STAGING'] ? TIMBER_STAGING_URL : TIMBER_PRODUCTION_URL
66
23
  CONTENT_TYPE = "application/msgpack".freeze
67
24
  USER_AGENT = "Timber Ruby/#{Timber::VERSION} (HTTP)".freeze
68
25
 
69
-
70
26
  # Instantiates a new HTTP log device that can be passed to {Timber::Logger#initialize}.
71
27
  #
72
28
  # The class maintains a buffer which is flushed in batches to the Timber API. 2
73
29
  # options control when the flush happens, `:batch_byte_size` and `:flush_interval`.
74
30
  # If either of these are surpassed, the buffer will be flushed.
75
31
  #
76
- # By default, the buffer will apply back pressure log messages are generated faster than
77
- # the client can delivery them. But you can drop messages instead by passing a
78
- # {DroppingSizedQueue} via the `:request_queue` option.
32
+ # By default, the buffer will apply back pressure when the rate of log messages exceeds
33
+ # the maximum delivery rate. If you don't want to sacrifice app performance in this case
34
+ # you can drop the log messages instead by passing a {DroppingSizedQueue} via the
35
+ # `:request_queue` option.
79
36
  #
80
37
  # @param api_key [String] The API key provided to you after you add your application to
81
38
  # [Timber](https://timber.io).
@@ -117,14 +74,16 @@ module Timber
117
74
  @flush_continuously = options[:flush_continuously] != false
118
75
  @flush_interval = options[:flush_interval] || 1 # 1 second
119
76
  @requests_per_conn = options[:requests_per_conn] || 2_500
120
- @msg_queue = LogMsgQueue.new(@batch_size)
77
+ @msg_queue = FlushableSizedQueue.new(@batch_size)
121
78
  @request_queue = options[:request_queue] || SizedQueue.new(3)
122
79
  @successive_error_count = 0
123
80
  @requests_in_flight = 0
124
81
  end
125
82
 
126
- # Write a new log line message to the buffer, and deliver if the msg exceeds the
127
- # payload limit.
83
+ # Write a new log line message to the buffer, and flush asynchronously if the
84
+ # message queue is full. We flush asynchronously because the maximum message batch
85
+ # size is constricted by the Timber API. The actual application limit is a multiple
86
+ # of this. Hence the `@request_queue`.
128
87
  def write(msg)
129
88
  @msg_queue.enqueue(msg)
130
89
 
@@ -135,23 +94,19 @@ module Timber
135
94
  ensure_flush_threads_are_started
136
95
 
137
96
  if @msg_queue.full?
138
- debug_logger.debug("Flushing HTTP buffer via write") if debug_logger
139
- flush
97
+ debug { "Flushing HTTP buffer via write" }
98
+ flush_async
140
99
  end
141
100
  true
142
101
  end
143
102
 
103
+ # Flush all log messages in the buffer synchronously. This method will not return
104
+ # until delivery of the messages has been successful. If you want to flush
105
+ # asynchronously see {#flush_async}.
144
106
  def flush
145
- @last_flush = Time.now
146
- msgs = @msg_queue.flush
147
- return if msgs.empty?
148
-
149
- req = Net::HTTP::Post.new(@timber_url.path)
150
- req['Authorization'] = authorization_payload
151
- req['Content-Type'] = CONTENT_TYPE
152
- req['User-Agent'] = USER_AGENT
153
- req.body = msgs.to_msgpack
154
- @request_queue.enq(req)
107
+ flush_async
108
+ wait_on_request_queue
109
+ true
155
110
  end
156
111
 
157
112
  # Closes the log device, cleans up, and attempts one last delivery.
@@ -162,20 +117,8 @@ module Timber
162
117
  # Flush all remaining messages
163
118
  flush
164
119
 
165
- # Kill the request_outlet thread gracefully. We do not want to kill it while a
166
- # request is inflight. Ideally we'd let it finish before we die.
167
- if @request_outlet_thread
168
- 4.times do
169
- if @requests_in_flight == 0 && @request_queue.size == 0
170
- @request_outlet_thread.kill
171
- break
172
- else
173
- debug_logger.error("Busy delivering the final log messages, " +
174
- "connection will close when complete.")
175
- sleep 1
176
- end
177
- end
178
- end
120
+ # Kill the request queue thread. Flushing ensures that no requests are pending.
121
+ @request_outlet_thread.kill if @request_outlet_thread
179
122
  end
180
123
 
181
124
  private
@@ -183,8 +126,16 @@ module Timber
183
126
  Timber::Config.instance.debug_logger
184
127
  end
185
128
 
129
+ # Convenience method for writing debug messages.
130
+ def debug(&block)
131
+ if debug_logger
132
+ message = yield
133
+ debug_logger.debug(message)
134
+ end
135
+ end
136
+
186
137
  # This is a convenience method to ensure the flush thread are
187
- # started. This is called lazily from #write so that we
138
+ # started. This is called lazily from {#write} so that we
188
139
  # only start the threads as needed, but it also ensures
189
140
  # threads are started after process forking.
190
141
  def ensure_flush_threads_are_started
@@ -199,6 +150,53 @@ module Timber
199
150
  end
200
151
  end
201
152
 
153
+ # Builds an HTTP request based on the current messages queued.
154
+ def build_request
155
+ msgs = @msg_queue.flush
156
+ return if msgs.empty?
157
+
158
+ req = Net::HTTP::Post.new(@timber_url.path)
159
+ req['Authorization'] = authorization_payload
160
+ req['Content-Type'] = CONTENT_TYPE
161
+ req['User-Agent'] = USER_AGENT
162
+ req.body = msgs.to_msgpack
163
+ req
164
+ end
165
+
166
+ # Flushes the message buffer asynchronously. The reason we provide this
167
+ # method is because the message buffer limit is constricted by the
168
+ # Timber API. The application limit is multiples of the buffer limit,
169
+ # hence the `@request_queue`, allowing us to buffer beyond the Timber API
170
+ # imposed limit.
171
+ def flush_async
172
+ @last_async_flush = Time.now
173
+ req = build_request
174
+ if !req.nil?
175
+ debug { "New request placed on queue" }
176
+ @request_queue.enq(req)
177
+ end
178
+ end
179
+
180
+ # Waits on the request queue. This is used in {#flush} to ensure
181
+ # the log data has been delivered before returning.
182
+ def wait_on_request_queue
183
+ # Wait 20 seconds
184
+ 40.times do |i|
185
+ if @request_queue.size == 0 && @requests_in_flight == 0
186
+ debug { "Request queue is empty and no requests are in flight, finish waiting" }
187
+ return true
188
+ end
189
+ debug do
190
+ "Request size #{@request_queue.size}, reqs in-flight #{@requests_in_flight}, " \
191
+ "continue waiting (iteration #{i + 1})"
192
+ end
193
+ sleep 0.5
194
+ end
195
+ end
196
+
197
+ # Flushes the message queue on an interval. You will notice that {#write} also
198
+ # flushes the buffer if it is full. This method takes note of this via the
199
+ # `@last_async_flush` variable as to not flush immediately after a write flush.
202
200
  def intervaled_flush
203
201
  # Wait specified time period before starting
204
202
  sleep @flush_interval
@@ -206,21 +204,25 @@ module Timber
206
204
  loop do
207
205
  begin
208
206
  if intervaled_flush_ready?
209
- debug_logger.debug("Flushing HTTP buffer via the interval") if debug_logger
210
- flush
207
+ debug { "Flushing HTTP buffer via the interval" }
208
+ flush_async
211
209
  end
212
210
 
213
211
  sleep(0.5)
214
212
  rescue Exception => e
215
- logger.error("Intervaled HTTP flush failed: #{e.inspect}\n\n#{e.backtrace}")
213
+ debug { "Intervaled HTTP flush failed: #{e.inspect}\n\n#{e.backtrace}" }
216
214
  end
217
215
  end
218
216
  end
219
217
 
218
+ # Determines if the loop in {#intervaled_flush} is ready to be flushed again. It
219
+ # uses the `@last_async_flush` variable to ensure that a flush does not happen
220
+ # too rapidly ({#write} also triggers a flush).
220
221
  def intervaled_flush_ready?
221
- @last_flush.nil? || (Time.now.to_f - @last_flush.to_f).abs >= @flush_interval
222
+ @last_async_flush.nil? || (Time.now.to_f - @last_async_flush.to_f).abs >= @flush_interval
222
223
  end
223
224
 
225
+ # Builds an `Net::HTTP` object to deliver requests over.
224
226
  def build_http
225
227
  http = Net::HTTP.new(@timber_url.host, @timber_url.port)
226
228
  http.set_debug_output(debug_logger) if debug_logger
@@ -231,30 +233,35 @@ module Timber
231
233
  http
232
234
  end
233
235
 
236
+ # Creates a loop that processes the `@request_queue` on an interval.
234
237
  def request_outlet
235
238
  loop do
236
239
  http = build_http
237
240
 
238
241
  begin
239
- debug_logger.info("Starting HTTP connection") if debug_logger
242
+ debug { "Starting HTTP connection" }
240
243
 
241
244
  http.start do |conn|
242
245
  deliver_requests(conn)
243
246
  end
244
247
  rescue => e
245
- debug_logger.error("#request_outlet error: #{e.message}") if debug_logger
248
+ debug { "#request_outlet error: #{e.message}" }
246
249
  ensure
247
- debug_logger.info("Finishing HTTP connection") if debug_logger
250
+ debug { "Finishing HTTP connection" }
248
251
  http.finish if http.started?
249
252
  end
250
253
  end
251
254
  end
252
255
 
256
+ # Creates a loop that delivers requests over an open (kept alive) HTTP connection.
257
+ # If the connection dies, the request is thrown back onto the queue and
258
+ # the method returns. It is the responsibility of the caller to implement retries
259
+ # and establish a new connection.
253
260
  def deliver_requests(conn)
254
261
  num_reqs = 0
255
262
 
256
263
  while num_reqs < @requests_per_conn
257
- debug_logger.info("Waiting on next request, threads waiting: #{@request_queue.num_waiting}") if debug_logger
264
+ debug { "Waiting on next request, threads waiting: #{@request_queue.num_waiting}" }
258
265
 
259
266
  # Blocks waiting for a request.
260
267
  req = @request_queue.deq
@@ -263,7 +270,7 @@ module Timber
263
270
  begin
264
271
  resp = conn.request(req)
265
272
  rescue => e
266
- debug_logger.error("#deliver_request error: #{e.message}") if debug_logger
273
+ debug { "#deliver_request error: #{e.message}" }
267
274
 
268
275
  @successive_error_count += 1
269
276
 
@@ -271,7 +278,7 @@ module Timber
271
278
  calculated_backoff = @successive_error_count * 2
272
279
  backoff = calculated_backoff > 30 ? 30 : calculated_backoff
273
280
 
274
- debug_logger.error("Backing off #{backoff} seconds, error ##{@successive_error_count}") if debug_logger
281
+ debug { "Backing off #{backoff} seconds, error ##{@successive_error_count}" }
275
282
 
276
283
  sleep backoff
277
284
 
@@ -284,10 +291,11 @@ module Timber
284
291
 
285
292
  @successive_error_count = 0
286
293
  num_reqs += 1
287
- debug_logger.info("Request successful: #{resp.code}") if debug_logger
294
+ debug { "Request successful: #{resp.code}" }
288
295
  end
289
296
  end
290
297
 
298
+ # Builds the `Authorization` header value for HTTP delivery to the Timber API.
291
299
  def authorization_payload
292
300
  @authorization_payload ||= "Basic #{Base64.urlsafe_encode64(@api_key).chomp}"
293
301
  end
@@ -0,0 +1,26 @@
1
+ module Timber
2
+ module LogDevices
3
+ class HTTP
4
+ # Works like SizedQueue, but drops message instead of blocking. Pass one of these in
5
+ # to {HTTP#intiialize} via the :request_queue option if you'd prefer to drop messages
6
+ # in the event of a buffer overflow instead of applying back pressure.
7
+ class DroppingSizedQueue < SizedQueue
8
+ # Returns true/false depending on whether the queue is full or not
9
+ def push(obj)
10
+ @mutex.synchronize do
11
+ return false unless @que.length < @max
12
+
13
+ @que.push obj
14
+ begin
15
+ t = @waiting.shift
16
+ t.wakeup if t
17
+ rescue ThreadError
18
+ retry
19
+ end
20
+ return true
21
+ end
22
+ end
23
+ end
24
+ end
25
+ end
26
+ end
@@ -0,0 +1,42 @@
1
+ module Timber
2
+ module LogDevices
3
+ class HTTP
4
+ # A simple thread-safe queue implementation that provides a #flush method.
5
+ # The built-in ruby Queue class does not provide a #flush method. It also
6
+ # implement thread waiting which is something we do not want. To keep things
7
+ # simple and straight-forward we designed our own simple queue class.
8
+ # @private
9
+ class FlushableSizedQueue
10
+ def initialize(max_size)
11
+ @lock = Mutex.new
12
+ @max_size = max_size
13
+ @array = []
14
+ end
15
+
16
+ # Adds a message to the queue
17
+ def enqueue(msg)
18
+ @lock.synchronize do
19
+ @array << msg
20
+ end
21
+ end
22
+
23
+ # Flushes all message from the queue and returns them.
24
+ def flush
25
+ @lock.synchronize do
26
+ old = @array
27
+ @array = []
28
+ return old
29
+ end
30
+ end
31
+
32
+ def full?
33
+ size >= @max_size
34
+ end
35
+
36
+ def size
37
+ @array.size
38
+ end
39
+ end
40
+ end
41
+ end
42
+ end