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 +4 -4
- data/README.md +38 -18
- data/lib/timber/log_devices/http.rb +183 -84
- data/lib/timber/version.rb +1 -1
- data/spec/timber/log_devices/http_spec.rb +47 -28
- metadata +2 -5
- data/lib/timber/log_devices/http/triggered_buffer.rb +0 -87
- data/spec/timber/log_devices/http/triggered_buffer_spec.rb +0 -58
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA1:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 2388bf1db8a727e78be858eb8ed03fd1dec10f95
|
4
|
+
data.tar.gz: 392fe3ea6f24f874849cad8ccc015ce989e67fda
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 856d7ebf1f309b905899dd8136a9af9ce60cab4d4c88c6d1b02009cc6b0677b51932366f22d4abdd4fd4f662e90f44021f8a9240f10a7c8a6886eddaf8ee417b
|
7
|
+
data.tar.gz: 24384bcea4caa8d460e1b4eab07047e2c6c94a181110d8b16c47eb1f672abdde6f8158e36a3e7bcf121410e1532e63083b810834d3ec7641c113d608ce1b215c
|
data/README.md
CHANGED
@@ -10,8 +10,12 @@
|
|
10
10
|
[](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. [
|
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
|
-
|
23
|
-
|
24
|
-
|
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
|
-
|
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
|
-
|
62
|
-
|
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.
|
72
|
-
2.
|
73
|
-
|
74
|
-
|
75
|
-
|
76
|
-
|
77
|
-
|
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:
|
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
|
-
|
112
|
-
|
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
|
6
|
-
#
|
7
|
-
#
|
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
|
-
|
12
|
-
|
13
|
-
|
14
|
-
|
15
|
-
|
16
|
-
|
17
|
-
|
18
|
-
|
19
|
-
|
20
|
-
|
21
|
-
|
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 =
|
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] :
|
36
|
-
#
|
37
|
-
# @option attributes [Symbol] :
|
38
|
-
#
|
39
|
-
#
|
40
|
-
#
|
41
|
-
#
|
42
|
-
#
|
43
|
-
#
|
44
|
-
#
|
45
|
-
#
|
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
|
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
|
-
#
|
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
|
-
@
|
61
|
-
|
62
|
-
|
63
|
-
|
64
|
-
|
65
|
-
@
|
66
|
-
|
67
|
-
|
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
|
-
|
78
|
-
|
79
|
-
|
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
|
-
|
89
|
-
if
|
90
|
-
|
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
|
-
@
|
98
|
-
|
99
|
-
|
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
|
106
|
-
|
107
|
-
|
108
|
-
|
109
|
-
|
110
|
-
|
111
|
-
|
112
|
-
|
113
|
-
|
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
|
-
|
117
|
-
|
118
|
-
|
119
|
-
|
120
|
-
|
121
|
-
|
122
|
-
|
123
|
-
|
124
|
-
|
125
|
-
|
126
|
-
|
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
|
data/lib/timber/version.rb
CHANGED
@@ -2,33 +2,31 @@ require "spec_helper"
|
|
2
2
|
|
3
3
|
describe Timber::LogDevices::HTTP do
|
4
4
|
describe "#initialize" do
|
5
|
-
it "should
|
6
|
-
|
7
|
-
|
8
|
-
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(:
|
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(
|
20
|
+
expect(msg_queue.flush).to eq(["test log message"])
|
23
21
|
end
|
24
22
|
|
25
|
-
context "with a low
|
26
|
-
let(:http) { described_class.new("MYKEY", :
|
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(:
|
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
|
38
|
+
it "should kill the threads" do
|
41
39
|
http.close
|
42
|
-
thread = http.instance_variable_get(:@
|
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(:
|
51
|
+
expect(http).to receive(:flush).exactly(1).times
|
51
52
|
http.close
|
52
53
|
end
|
53
54
|
end
|
54
55
|
|
55
|
-
|
56
|
-
|
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
|
-
|
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
|
-
|
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 =>
|
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
|
-
|
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.
|
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-
|
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
|