timber 1.0.8 → 1.0.9
Sign up to get free protection for your applications and to get access to all the features.
- 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
|
[![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. [
|
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
|