timber 1.0.8 → 1.0.9

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.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: 9e48b303535350b36a68d4af296da5a9ee1ddfe1
4
- data.tar.gz: accd259a2827a8a466e5cb2aadfc93ac8d350cb9
3
+ metadata.gz: 2388bf1db8a727e78be858eb8ed03fd1dec10f95
4
+ data.tar.gz: 392fe3ea6f24f874849cad8ccc015ce989e67fda
5
5
  SHA512:
6
- metadata.gz: 2f4bf4cf69e07973acc22b59892bcb0b33c76af23de8023fa6fde65807752d7f2d5739ecbed720e0c344bf2c14eda2b994772ec0450fc36c40922d9681e65ea1
7
- data.tar.gz: 5917a43860fe36328e1a5246596eca5124ce4a4ca589f41a7b1a3da529484c5b04fab5760f96e98d74f64e90a75b782b7188f2057aa6d2423705a9f03f151ed4
6
+ metadata.gz: 856d7ebf1f309b905899dd8136a9af9ce60cab4d4c88c6d1b02009cc6b0677b51932366f22d4abdd4fd4f662e90f44021f8a9240f10a7c8a6886eddaf8ee417b
7
+ data.tar.gz: 24384bcea4caa8d460e1b4eab07047e2c6c94a181110d8b16c47eb1f672abdde6f8158e36a3e7bcf121410e1532e63083b810834d3ec7641c113d608ce1b215c
data/README.md CHANGED
@@ -10,8 +10,12 @@
10
10
  [![View docs](https://img.shields.io/badge/docs-viewdocs-blue.svg?style=flat-square "Viewdocs")](http://www.rubydoc.info/github/timberio/timber-ruby)
11
11
 
12
12
 
13
+ **Timber is in beta testing. If interested, please email beta@timber.io**
14
+
15
+
13
16
  1. [What is timber?](#what-is-timber)
14
- 2. [Why timber?](#why-timber)
17
+ 2. [How it works](#how-it-works)
18
+ 3. [Why timber?](#why-timber)
15
19
  4. [Logging Custom Events](#logging-custom-events)
16
20
  5. [The Timber Console / Pricing](#the-timber-console--pricing)
17
21
  6. [Install](#install)
@@ -19,11 +23,22 @@
19
23
 
20
24
  ## What is Timber?
21
25
 
22
- [Timber](http://timber.io) is a different kind of logging platform; it goes beyond traditional
23
- logging by automatically enriching your logs with application level metadata, turning them
24
- into rich, structured events without altering the essence of logging.
26
+ Using your logs shouldn't be a time consuming, frustrating process! If you were like us, it goes
27
+ something like this:
28
+
29
+ > How do I access my logs? Which provider should I use? Why is it so expensive?
30
+ > Why is searching so difficult and time consuming? Should I structure my logs? Which format
31
+ > should I use? Will they still be human readable? What if the structure changes? What about logs
32
+ > from 3rd party libraries? Ahhh!!!
33
+
34
+ Timber solves this by providing a complete, managed, end-to-end logging solution that marries
35
+ a beautiful, *fast*, console with libraries that automatically structure and enrich your logs.
25
36
 
26
- For example, it turns this:
37
+
38
+ ## How it works
39
+
40
+ The Timber ruby library takes care of all of the log structuring madness. For example,
41
+ it turns this Rails log line:
27
42
 
28
43
  ```
29
44
  Completed 200 OK in 117ms (Views: 85.2ms | ActiveRecord: 25.3ms)
@@ -58,8 +73,10 @@ Into this:
58
73
  }
59
74
  ```
60
75
 
61
- It does the same for `http requests`, `sql queries`, `exceptions`, `template renderings`,
62
- and any other event your framework logs.
76
+ Notice we preserve the original log message. When viewing this event in the
77
+ [Timber Console](https://timber.io), you'll see the simple, human readable line with the
78
+ ability to view, and use, the attached structured data! Also, notice how rich the event is.
79
+ Beecause we're inside your application, we can capture data beyond what's in the log line.
63
80
 
64
81
  (for a full list see [`Timber::Events`](lib/timber/events))
65
82
 
@@ -68,13 +85,14 @@ and any other event your framework logs.
68
85
 
69
86
  Glad you asked! :)
70
87
 
71
- 1. It's application aware and enriches your logs with data you can't get otherwise.
72
- 2. It defines a shared schema across all of our libraries. Meaning your log data, across all
73
- applications, is normalized.
74
- 3. It does not alter the original log message, giving you the best of both worlds: human
75
- readable logs *and* rich structured events.
76
- 4. It's completely transparent with absolutely no vendor lock-in or risk of code debt. It
77
- does not introduce a special API, it's just good ol' loggin'.
88
+ 1. Human readable logs *and* rich structured data. You don't have to choose.
89
+ 2. Data beyond what's in the log line itself making your logs exceptionally useful.
90
+ 3. Normalized log data. Timber enforces a strict schema, meaning your log data, across all apps
91
+ and languages will be normalized.
92
+ 4. Absolutely no lock-in or risk of code debt. No fancy API, no proprietary data format locked
93
+ away in our servers, Timber is just good ol' loggin'.
94
+ 5. Fully managed, all the way from the log messages to the console you use. No fragile parsing
95
+ rules or complicated interfaces.
78
96
 
79
97
 
80
98
  ## Logging Custom Events
@@ -89,7 +107,7 @@ Logger.warn "Payment rejected for customer abcd1234, reason: Card expired"
89
107
 
90
108
  # Structured hash
91
109
  Logger.warn message: "Payment rejected", type: :payment_rejected,
92
- data: %{customer_id: "abcd1234", amount: 100, reason: "Card expired"}
110
+ data: {customer_id: "abcd1234", amount: 100, reason: "Card expired"}
93
111
 
94
112
  # Using a Struct
95
113
  PaymentRejectedEvent = Struct.new(:customer_id, :amount, :reason) do
@@ -108,8 +126,8 @@ No mention of Timber anywhere!
108
126
 
109
127
  > What good is structured log data if you can't search and visualize it?
110
128
 
111
- Enter [the Timber Console](https://timber.io). It's a modern, fast, and beautiful console for
112
- searching and visualizing your logs.
129
+ The [Timber Console](https://timber.io) is *fast*, modern, and beautiful console designed
130
+ specifically for this library.
113
131
 
114
132
  A few example queries:
115
133
 
@@ -120,7 +138,7 @@ A few example queries:
120
138
 
121
139
  > This is all gravy, but wouldn't the extra data get expensive?
122
140
 
123
- If you opt use the [Timber Console](https://timber.io), we only charge for
141
+ If you opt to use the [Timber Console](https://timber.io), we only charge for
124
142
  the size of the `message`, `dt`, and `event.custom` attributes. Everything else is
125
143
  stored at no cost to you. [Say wha?!](http://i.giphy.com/l0HlL2vlfpWI0meJi.gif). This ensures
126
144
  pricing remains predictable. We charge per GB sent to us and retained. No user limits,
@@ -131,6 +149,8 @@ For more details checkout out [timber.io](https://timber.io).
131
149
 
132
150
  ## Install
133
151
 
152
+ **Timber is in beta testing. If interested, please email beta@timber.io**
153
+
134
154
  ### 1. Install the gem:
135
155
 
136
156
  ```ruby
@@ -1,130 +1,229 @@
1
- require "timber/log_devices/http/triggered_buffer"
2
-
3
1
  module Timber
4
2
  module LogDevices
5
- # A log device that buffers and delivers log messages over HTTPS to the Timber API in batches.
6
- # The buffer and delivery strategy are very efficient and the log messages will be delivered in
7
- # msgpack format.
3
+ # A log device that buffers and delivers log messages over HTTPS to the Timber API.
4
+ # It uses batches, keep-alive connections, and messagepack to delivery logs with
5
+ # high-throughput and little overhead.
8
6
  #
9
7
  # See {#initialize} for options and more details.
10
8
  class HTTP
11
- API_URI = URI.parse(ENV["TIMBER_INGESTION_URL"] || "https://logs.timber.io/frames")
12
- CONTENT_TYPE = "application/x-timber-msgpack-frame-1".freeze
13
- CONNECTION_HEADER = "keep-alive".freeze
14
- USER_AGENT = "Timber Ruby Gem/#{Timber::VERSION}".freeze
15
- HTTPS = Net::HTTP.new(API_URI.host, API_URI.port).tap do |https|
16
- https.use_ssl = true
17
- https.read_timeout = 30
18
- https.ssl_timeout = 10
19
- # Ruby 1.9.X doesn't have this setting.
20
- if https.respond_to?(:keep_alive_timeout=)
21
- https.keep_alive_timeout = 60
9
+ # @private
10
+ class LogMsgQueue
11
+ MAX_MSG_BYTES = 50_000 # 50kb
12
+
13
+ def initialize(max_bytes)
14
+ @lock = Mutex.new
15
+ @max_bytes = max_bytes
16
+ @array = []
17
+ @bytesize = 0
18
+ end
19
+
20
+ def enqueue(msg)
21
+ if msg.bytesize > MAX_MSG_BYTES
22
+ raise ArgumentError.new("Log message exceeds the #{MAX_MSG_BYTES} bytes limit")
23
+ end
24
+
25
+ @lock.synchronize do
26
+ @array << msg
27
+ @bytesize += msg.bytesize
28
+ end
29
+ end
30
+
31
+ def flush
32
+ @lock.synchronize do
33
+ old = @array
34
+ @array = []
35
+ @bytesize = 0
36
+ return old
37
+ end
38
+ end
39
+
40
+ def full?
41
+ @lock.synchronize do
42
+ @bytesize >= @max_bytes
43
+ end
44
+ end
45
+
46
+ def size
47
+ @array.size
48
+ end
49
+ end
50
+
51
+ # Works like SizedQueue, but drops message instead of blocking. Pass one of these in
52
+ # to {HTTP#intiialize} via the :request_queue option if you'd prefer to drop messages
53
+ # in the event of a buffer overflow instead of applying back pressure.
54
+ class DroppingSizedQueue < SizedQueue
55
+ # Returns true/false depending on whether the queue is full or not
56
+ def push(obj)
57
+ @mutex.synchronize do
58
+ return false unless @que.length < @max
59
+
60
+ @que.push obj
61
+ begin
62
+ t = @waiting.shift
63
+ t.wakeup if t
64
+ rescue ThreadError
65
+ retry
66
+ end
67
+ return true
68
+ end
22
69
  end
23
- https.open_timeout = 10
24
70
  end
71
+
72
+ TIMBER_URL = "https://logs.timber.io/frames".freeze
73
+ CONTENT_TYPE = "application/x-timber-msgpack-frame-1".freeze
74
+ USER_AGENT = "Timber Ruby Gem/#{Timber::VERSION}".freeze
25
75
  DELIVERY_FREQUENCY_SECONDS = 2.freeze
26
- RETRY_LIMIT = 3.freeze
76
+ RETRY_LIMIT = 5.freeze
27
77
  BACKOFF_RATE_SECONDS = 3.freeze
28
78
 
29
79
 
30
80
  # Instantiates a new HTTP log device that can be passed to {Timber::Logger#initialize}.
31
81
  #
82
+ # The class maintains a buffer which is flushed in batches to the Timber API. 2
83
+ # options control when the flush happens, `:batch_byte_size` and `:flush_interval`.
84
+ # If either of these are surpassed, the buffer will be flushed.
85
+ #
86
+ # By default, the buffer will apply back pressure log messages are generated faster than
87
+ # the client can delivery them. But you can drop messages instead by passing a
88
+ # {DroppingSizedQueue} via the `:request_queue` option.
89
+ #
32
90
  # @param api_key [String] The API key provided to you after you add your application to
33
91
  # [Timber](https://timber.io).
34
92
  # @param [Hash] options the options to create a HTTP log device with.
35
- # @option attributes [Symbol] :payload_limit_bytes Determines the maximum size in bytes that
36
- # and HTTP payload can be. Please see {TriggereBuffer#initialize} for the default.
37
- # @option attributes [Symbol] :buffer_limit_bytes Determines the maximum size of the total
38
- # buffer. This should be many times larger than the `:payload_limit_bytes`.
39
- # Please see {TriggereBuffer#initialize} for the default.
40
- # @option attributes [Symbol] :buffer_overflow_handler (nil) When a single message exceeds
41
- # `:payload_limit_bytes` or the entire buffer exceeds `:buffer_limit_bytes`, the Proc
42
- # passed to this option will be called with the msg that would overflow the buffer. See
43
- # the examples on how to use this properly.
44
- # @option attributes [Symbol] :delivery_frequency_seconds (2) How often the client should
45
- # attempt to deliver logs to the Timber API. The HTTP client buffers logs between calls.
93
+ # @option attributes [Symbol] :batch_byte_size Determines the maximum size in bytes for
94
+ # each HTTP payload. If the buffer exceeds this limit a delivery will be attempted.
95
+ # @option attributes [Symbol] :debug Whether to print debug output or not. This is also
96
+ # inferred from ENV['debug']. Output will be sent to `Timber::Config.logger`.
97
+ # @option attributes [Symbol] :flush_interval (2) How often the client should
98
+ # attempt to deliver logs to the Timber API. The HTTP client buffers logs and this
99
+ # options represents how often that will happen, assuming `:batch_byte_size` is not met.
100
+ # @option attributes [Symbol] :requests_per_conn The number of requests to send over a
101
+ # single persistent connection. After this number is met, the connection will be closed
102
+ # and a new one will be opened.
103
+ # @option attributes [Symbol] :request_queue The request queue object that queues Net::HTTP
104
+ # requests for delivery. By deafult this is a `SizedQueue` of size `3`. Meaning once
105
+ # 3 requests are placed on the queue for delivery, back pressure will be applied. IF
106
+ # you'd prefer to drop messages instead, pass a {DroppingSizedQueue}. See examples for
107
+ # an example.
108
+ # @option attributes [Symbol] :timber_url The Timber URL to delivery the log lines. The
109
+ # default is set via {TIMBER_URL}.
46
110
  #
47
111
  # @example Basic usage
48
112
  # Timber::Logger.new(Timber::LogDevices::HTTP.new("my_timber_api_key"))
49
113
  #
50
- # @example Handling buffer overflows
51
- # # Persist overflowed lines to a file
52
- # # Note: You could write these to any permanent storage.
53
- # overflow_log_path = "/path/to/my/overflow_log.log"
54
- # overflow_handler = Proc.new { |log_line_msg| File.write(overflow_log_path, log_line_ms) }
114
+ # @example Dropping messages instead of applying back pressure
55
115
  # http_log_device = Timber::LogDevices::HTTP.new("my_timber_api_key",
56
- # buffer_overflow_handler: overflow_handler)
116
+ # request_queue: Timber::LogDevices::HTTP::DroppingSizedQueue.new(3))
57
117
  # Timber::Logger.new(http_log_device)
58
118
  def initialize(api_key, options = {})
59
119
  @api_key = api_key
60
- @buffer = TriggeredBuffer.new(
61
- payload_limit_bytes: options[:payload_limit_bytes],
62
- limit_bytes: options[:buffer_limit_bytes],
63
- overflow_handler: options[:buffer_overflow_handler]
64
- )
65
- @delivery_interval_thread = Thread.new do
66
- loop do
67
- sleep(options[:delivery_frequency_seconds] || DELIVERY_FREQUENCY_SECONDS)
68
-
69
- @last_messages_overflow_count = 0
70
- messages_overflown_count = @buffer.messages_overflown_count
71
- if messages_overflown_count >= @last_messages_overflow_count
72
- difference = messages_overflown_count - @last_messages_overflow_count
73
- @last_messages_overflow_count = messages_overflown_count
74
- logger.warn("Timber HTTP buffer has overflown #{difference} times")
75
- end
120
+ @debug = options[:debug] || ENV['debug']
121
+ @timber_url = URI.parse(options[:timber_url] || ENV['TIMBER_URL'] || TIMBER_URL)
122
+ @batch_byte_size = options[:batch_byte_size] || 3_000_000 # 3mb
123
+ @flush_interval = options[:flush_interval] || 2 # 2 seconds
124
+ @requests_per_conn = options[:requests_per_conn] || 1_000
125
+ @msg_queue = LogMsgQueue.new(@batch_byte_size)
126
+ @request_queue = options[:request_queue] || SizedQueue.new(3)
127
+ @req_in_flight = 0
76
128
 
77
- buffer_for_delivery = @buffer.reserve
78
- if buffer_for_delivery
79
- deliver(buffer_for_delivery)
80
- end
81
- end
129
+ if options[:threads] != false
130
+ @outlet_thread = Thread.new { outlet }
131
+ @flush_thread = Thread.new { intervaled_flush }
82
132
  end
83
133
  end
84
134
 
85
135
  # Write a new log line message to the buffer, and deliver if the msg exceeds the
86
136
  # payload limit.
87
137
  def write(msg)
88
- buffer_for_delivery = @buffer.write(msg)
89
- if buffer_for_delivery
90
- deliver(buffer_for_delivery)
138
+ @msg_queue.enqueue(msg)
139
+ if @msg_queue.full?
140
+ flush
91
141
  end
92
142
  true
93
143
  end
94
144
 
95
145
  # Closes the log device, cleans up, and attempts one last delivery.
96
146
  def close
97
- @delivery_interval_thread.kill
98
- buffer_for_delivery = @buffer.reserve
99
- if buffer_for_delivery
100
- deliver(buffer_for_delivery)
101
- end
147
+ @flush_thread.kill if @flush_thread
148
+ @outlet_thread.kill if @outlet_thread
149
+ flush
102
150
  end
103
151
 
104
152
  private
105
- def deliver(body)
106
- Thread.new do
107
- RETRY_LIMIT.times do |try_index|
108
- request = Net::HTTP::Post.new(API_URI.request_uri).tap do |req|
109
- req['Authorization'] = authorization_payload
110
- req['Connection'] = CONNECTION_HEADER
111
- req['Content-Type'] = CONTENT_TYPE
112
- req['User-Agent'] = USER_AGENT
113
- req.body = body
153
+ def debug?
154
+ !@debug.nil?
155
+ end
156
+
157
+ def flush
158
+ msgs = @msg_queue.flush
159
+ return if msgs.empty?
160
+
161
+ body = ""
162
+ msgs.each do |msg|
163
+ body << msg
164
+ end
165
+
166
+ req = Net::HTTP::Post.new(@timber_url.path)
167
+ req['Authorization'] = authorization_payload
168
+ req['Content-Type'] = CONTENT_TYPE
169
+ req['User-Agent'] = USER_AGENT
170
+ req.body = body
171
+ @request_queue.enq(req)
172
+ @last_flush = Time.now
173
+ end
174
+
175
+ def intervaled_flush
176
+ # Wait specified time period before starting
177
+ sleep @flush_interval
178
+ loop do
179
+ begin
180
+ if intervaled_flush_ready?
181
+ flush
114
182
  end
183
+ sleep(0.1)
184
+ rescue Exception => e
185
+ logger.error("Timber intervaled flush failed: #{e.inspect}")
186
+ end
187
+ end
188
+ end
189
+
190
+ def intervaled_flush_ready?
191
+ @last_flush.nil? || (Time.now.to_f - @last_flush.to_f).abs >= @flush_interval
192
+ end
193
+
194
+ def outlet
195
+ loop do
196
+ http = Net::HTTP.new(@timber_url.host, @timber_url.port)
197
+ http.set_debug_output(logger) if debug?
198
+ http.use_ssl = true if @timber_url.scheme == 'https'
199
+ http.read_timeout = 30
200
+ http.ssl_timeout = 10
201
+ http.open_timeout = 10
115
202
 
116
- res = HTTPS.request(request)
117
- code = res.code.to_i
118
- if code < 200 || code >= 300
119
- try = try_index + 1
120
- logger.debug("Timber HTTP delivery failed, try #{try} - #{res.code}: #{res.body}")
121
- sleep(try * BACKOFF_RATE_SECONDS)
122
- else
123
- @buffer.remove(body)
124
- logger.debug("Timber HTTP delivery successful - #{code}")
125
- logger.debug("Timber new buffer size - #{@buffer.total_bytesize}")
126
- break # exit the loop
203
+ begin
204
+ http.start do |conn|
205
+ num_reqs = 0
206
+ while num_reqs < @requests_per_conn
207
+ #Blocks waiting for a request.
208
+ req = @request_queue.deq
209
+ @req_in_flight += 1
210
+ resp = nil
211
+ begin
212
+ resp = conn.request(req)
213
+ rescue => e
214
+ logger.error("Timber request error: #{e.message}") if debug?
215
+ next
216
+ ensure
217
+ @req_in_flight -= 1
218
+ end
219
+ num_reqs += 1
220
+ logger.info("Timber request successful: #{resp.code}") if debug?
221
+ end
127
222
  end
223
+ rescue => e
224
+ logger.error("Timber request error: #{e.message}") if debug?
225
+ ensure
226
+ http.finish if http.started?
128
227
  end
129
228
  end
130
229
  end
@@ -1,3 +1,3 @@
1
1
  module Timber
2
- VERSION = "1.0.8"
2
+ VERSION = "1.0.9"
3
3
  end
@@ -2,33 +2,31 @@ require "spec_helper"
2
2
 
3
3
  describe Timber::LogDevices::HTTP do
4
4
  describe "#initialize" do
5
- it "should start a thread for delivery" do
6
- expect_any_instance_of(described_class).to receive(:deliver).at_least(1).times.and_return(true)
7
- http = described_class.new("MYKEY", delivery_frequency_seconds: 0.1)
8
- thread = http.instance_variable_get(:@delivery_interval_thread)
5
+ it "should initialize properly" do
6
+ http = described_class.new("MYKEY", flush_interval: 0.1)
7
+ thread = http.instance_variable_get(:@flush_thread)
8
+ expect(thread).to be_alive
9
+ thread = http.instance_variable_get(:@flush_thread)
9
10
  expect(thread).to be_alive
10
-
11
- http.write("my log message")
12
- sleep 0.3 # too fast!
13
11
  end
14
12
  end
15
13
 
16
14
  describe "#write" do
17
15
  let(:http) { described_class.new("MYKEY") }
18
- let(:buffer) { http.instance_variable_get(:@buffer) }
16
+ let(:msg_queue) { http.instance_variable_get(:@msg_queue) }
19
17
 
20
18
  it "should buffer the messages" do
21
19
  http.write("test log message")
22
- expect(buffer.reserve).to eq("test log message")
20
+ expect(msg_queue.flush).to eq(["test log message"])
23
21
  end
24
22
 
25
- context "with a low payload limit" do
26
- let(:http) { described_class.new("MYKEY", :payload_limit_bytes => 20) }
23
+ context "with a low batch byte size" do
24
+ let(:http) { described_class.new("MYKEY", :batch_byte_size => 20) }
27
25
 
28
26
  it "should attempt a delivery when the payload limit is exceeded" do
29
27
  message = "a" * 19
30
28
  http.write(message)
31
- expect(http).to receive(:deliver).exactly(1).times.with(message)
29
+ expect(http).to receive(:flush).exactly(1).times
32
30
  http.write("my log message")
33
31
  end
34
32
  end
@@ -37,9 +35,12 @@ describe Timber::LogDevices::HTTP do
37
35
  describe "#close" do
38
36
  let(:http) { described_class.new("MYKEY") }
39
37
 
40
- it "should kill the delivery thread the messages" do
38
+ it "should kill the threads" do
41
39
  http.close
42
- thread = http.instance_variable_get(:@delivery_interval_thread)
40
+ thread = http.instance_variable_get(:@flush_thread)
41
+ sleep 0.1 # too fast!
42
+ expect(thread).to_not be_alive
43
+ thread = http.instance_variable_get(:@outlet_thread)
43
44
  sleep 0.1 # too fast!
44
45
  expect(thread).to_not be_alive
45
46
  end
@@ -47,39 +48,57 @@ describe Timber::LogDevices::HTTP do
47
48
  it "should attempt a delivery" do
48
49
  message = "a" * 19
49
50
  http.write(message)
50
- expect(http).to receive(:deliver).exactly(1).times.with(message)
51
+ expect(http).to receive(:flush).exactly(1).times
51
52
  http.close
52
53
  end
53
54
  end
54
55
 
55
- describe "#deliver" do
56
- let(:http) { described_class.new("MYKEY") }
56
+ # Testing a private method because it helps break down our tests
57
+ describe "#flush" do
58
+ it "should add a request to the queue" do
59
+ http = described_class.new("MYKEY", threads: false)
60
+ http.write("This is a log message")
61
+ http.send(:flush)
62
+ request_queue = http.instance_variable_get(:@request_queue)
63
+ request = request_queue.deq
64
+ expect(request).to be_kind_of(Net::HTTP::Post)
65
+ expect(request.body).to eq("This is a log message")
57
66
 
58
- after(:each) { http.close }
67
+ message_queue = http.instance_variable_get(:@msg_queue)
68
+ expect(message_queue.size).to eq(0)
69
+ end
70
+ end
71
+
72
+ # Testing a private method because it helps break down our tests
73
+ describe "#intervaled_flush" do
74
+ it "should start a intervaled flush thread and flush on an interval" do
75
+ http = described_class.new("MYKEY", flush_interval: 0.1)
76
+ expect(http).to receive(:flush).exactly(1).times
77
+ sleep 0.15 # too fast!
78
+ mock = expect(http).to receive(:flush).exactly(1).times
79
+ sleep 0.15 # too fast!
80
+ end
81
+ end
59
82
 
60
- it "should delivery properly and flush the buffer" do
83
+ # Outlet
84
+ describe "#outlet" do
85
+ it "should start a intervaled flush thread and flush on an interval" do
61
86
  stub = stub_request(:post, "https://logs.timber.io/frames").
62
87
  with(
63
- :body => "test log message",
88
+ :body => 'test log message',
64
89
  :headers => {
65
90
  'Authorization' => 'Basic TVlLRVk=',
66
- 'Connection' => 'keep-alive',
67
91
  'Content-Type' => 'application/x-timber-msgpack-frame-1',
68
92
  'User-Agent' => "Timber Ruby Gem/#{Timber::VERSION}"
69
93
  }
70
94
  ).
71
95
  to_return(:status => 200, :body => "", :headers => {})
72
96
 
97
+ http = described_class.new("MYKEY", flush_interval: 0.1)
73
98
  http.write("test log message")
74
- buffer = http.instance_variable_get(:@buffer)
75
- buffers = buffer.instance_variable_get(:@buffers)
76
- expect(buffers.size).to eq(1)
77
- body = buffer.reserve
78
- thread = http.send(:deliver, body)
79
- thread.join
99
+ sleep 0.2
80
100
 
81
101
  expect(stub).to have_been_requested.times(1)
82
- expect(buffers.size).to eq(0)
83
102
  end
84
103
  end
85
104
  end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: timber
3
3
  version: !ruby/object:Gem::Version
4
- version: 1.0.8
4
+ version: 1.0.9
5
5
  platform: ruby
6
6
  authors:
7
7
  - Timber Technologies, Inc.
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2016-12-16 00:00:00.000000000 Z
11
+ date: 2016-12-19 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: msgpack
@@ -62,7 +62,6 @@ files:
62
62
  - lib/timber/frameworks/rails.rb
63
63
  - lib/timber/log_devices.rb
64
64
  - lib/timber/log_devices/http.rb
65
- - lib/timber/log_devices/http/triggered_buffer.rb
66
65
  - lib/timber/log_entry.rb
67
66
  - lib/timber/logger.rb
68
67
  - lib/timber/probe.rb
@@ -95,7 +94,6 @@ files:
95
94
  - spec/support/timecop.rb
96
95
  - spec/support/webmock.rb
97
96
  - spec/timber/events_spec.rb
98
- - spec/timber/log_devices/http/triggered_buffer_spec.rb
99
97
  - spec/timber/log_devices/http_spec.rb
100
98
  - spec/timber/logger_spec.rb
101
99
  - spec/timber/probes/action_controller_log_subscriber_spec.rb
@@ -143,7 +141,6 @@ test_files:
143
141
  - spec/support/timecop.rb
144
142
  - spec/support/webmock.rb
145
143
  - spec/timber/events_spec.rb
146
- - spec/timber/log_devices/http/triggered_buffer_spec.rb
147
144
  - spec/timber/log_devices/http_spec.rb
148
145
  - spec/timber/logger_spec.rb
149
146
  - spec/timber/probes/action_controller_log_subscriber_spec.rb
@@ -1,87 +0,0 @@
1
- require "monitor"
2
-
3
- module Timber
4
- module LogDevices
5
- class HTTP
6
- # Maintains a triggered buffer, where the trigger is {PAYLOAD_LIMIT_BYTES}. Once the buffer
7
- # exceeds this limit it will lock and return that buffer up to that point while still making
8
- # a new buffer available for writes. This ensures that the HTTP client can attempt to deliver
9
- # the buffer contents without blocking execution of the application.
10
- #
11
- # If the overall buffer exceeeds the overall limit (specified by the `:limit_bytes` option),
12
- # then a buffer overflow is triggered. This can be customized using the `:overflow_handler`
13
- # option.
14
- class TriggeredBuffer
15
- DEFAULT_PAYLOAD_LIMIT_BYTES = 5_000_000 # 5mb, the Timber API will not accept messages larger than this
16
- DEFAULT_LIMIT_BYTES = 50_000_000 # 50mb
17
-
18
- attr_reader :messages_overflown_count
19
-
20
- def initialize(options = {})
21
- @buffers = []
22
- @monitor = Monitor.new
23
- @payload_limit_bytes = options[:payload_limit_bytes] || DEFAULT_PAYLOAD_LIMIT_BYTES
24
- @limit_bytes = options[:limit_bytes] || DEFAULT_LIMIT_BYTES
25
- @overflow_handler = options[:overflow_handler]
26
- @messages_overflown_count = 0
27
- end
28
-
29
- def write(msg)
30
- if msg.bytesize > @payload_limit_bytes || (msg.bytesize + total_bytesize) > @limit_bytes
31
- handle_overflow(msg)
32
- return nil
33
- end
34
-
35
- @monitor.synchronize do
36
- buffer = writable_buffer
37
- if @buffers == [] || buffer.nil? || buffer.frozen?
38
- @buffers << msg
39
- nil
40
- elsif (buffer.bytesize + msg.bytesize) > @payload_limit_bytes
41
- @buffers << msg
42
- buffer.freeze
43
- else
44
- buffer << msg
45
- nil
46
- end
47
- end
48
- end
49
-
50
- def reserve
51
- @monitor.synchronize do
52
- buffer = writable_buffer
53
- if buffer
54
- buffer.freeze
55
- end
56
- end
57
- end
58
-
59
- def remove(buffer)
60
- @monitor.synchronize do
61
- @buffers.delete(buffer)
62
- end
63
- end
64
-
65
- def total_bytesize
66
- @buffers.reduce(0) { |acc, buffer| acc + buffer.bytesize }
67
- end
68
-
69
- private
70
- def writable_buffer
71
- @buffers.find { |buffer| !buffer.frozen? }
72
- end
73
-
74
- def handle_overflow(msg)
75
- @messages_overflown_count += 1
76
- if @overflow_handler
77
- @overflow_handler.call(msg)
78
- end
79
- end
80
-
81
- def logger
82
- Config.instance.logger
83
- end
84
- end
85
- end
86
- end
87
- end
@@ -1,58 +0,0 @@
1
- require "spec_helper"
2
-
3
- describe Timber::LogDevices::HTTP::TriggeredBuffer do
4
- describe "#write" do
5
- it "should trigger a buffer overflow for large messages" do
6
- buffer = described_class.new(:payload_limit_bytes => 10)
7
- msg = "a" * 11
8
- expect(buffer).to receive(:handle_overflow).exactly(1).times.with(msg)
9
- buffer.write(msg)
10
- end
11
-
12
- it "should trigger a buffer overflow when exceeding the limit" do
13
- buffer = described_class.new(:limit_bytes => 10)
14
- msg = "a" * 11
15
- expect(buffer).to receive(:handle_overflow).exactly(1).times.with(msg)
16
- buffer.write(msg)
17
- end
18
-
19
- it "should start a new buffer when empty and append when not" do
20
- buffer = described_class.new
21
- result = buffer.write("test")
22
- expect(result).to be_nil
23
- expect(buffer.send(:writable_buffer)).to eq("test")
24
- result = buffer.write("again")
25
- expect(result).to be_nil
26
- expect(buffer.send(:writable_buffer)).to eq("testagain")
27
- end
28
-
29
- it "should return the old buffer when it has exceeded it's limit" do
30
- buffer = described_class.new(:payload_limit_bytes => 10)
31
- msg = "a" * 6
32
- result = buffer.write(msg)
33
- expect(result).to be_nil
34
- result = buffer.write(msg)
35
- expect(result).to eq(msg)
36
- expect(result).to be_frozen
37
- end
38
-
39
- it "should write a new buffer when the latest is frozen" do
40
- buffer = described_class.new
41
- buffer.write("test")
42
- result = buffer.reserve
43
- expect(result).to eq("test")
44
- buffer.write("again")
45
- expect(buffer.send(:writable_buffer)).to eq("again")
46
- end
47
- end
48
-
49
- describe "#reserve" do
50
- it "should reserve the latest buffer and freeze it" do
51
- buffer = described_class.new
52
- buffer.write("test")
53
- result = buffer.reserve
54
- expect(result).to eq("test")
55
- expect(result).to be_frozen
56
- end
57
- end
58
- end