timber 2.0.24 → 2.1.0.rc1

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