timber 1.0.11 → 1.0.12
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/lib/timber/log_devices/http.rb +16 -30
- data/lib/timber/log_entry.rb +4 -0
- data/lib/timber/logger.rb +8 -25
- data/lib/timber/version.rb +1 -1
- data/spec/timber/log_devices/http_spec.rb +28 -13
- data/spec/timber/logger_spec.rb +2 -26
- metadata +1 -1
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA1:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 66168776f4469f5a0ed9795a376b7f29fefe7a07
|
4
|
+
data.tar.gz: 3782864585581e766834c6a4e907b7a91aac4006
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: c3439222a545671693a4dcf0a1124627df087e7de306775a2d682c03ca963b598f202959ea0137af793ce91ef5db3634f9d4369c859eb55196a65c0d6f7ba3b5
|
7
|
+
data.tar.gz: c8cecdddcc719f6b5f84453ee784e9c5b1a5d2a4b34d6d7c69868c0cf05c8789a3555de2626b6f11d52f88bc900318dc76e77926dce5046dc5f01c47b7cdd457
|
@@ -8,23 +8,15 @@ module Timber
|
|
8
8
|
class HTTP
|
9
9
|
# @private
|
10
10
|
class LogMsgQueue
|
11
|
-
|
12
|
-
|
13
|
-
def initialize(max_bytes)
|
11
|
+
def initialize(max_size)
|
14
12
|
@lock = Mutex.new
|
15
|
-
@
|
13
|
+
@max_size = max_size
|
16
14
|
@array = []
|
17
|
-
@bytesize = 0
|
18
15
|
end
|
19
16
|
|
20
17
|
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
18
|
@lock.synchronize do
|
26
19
|
@array << msg
|
27
|
-
@bytesize += msg.bytesize
|
28
20
|
end
|
29
21
|
end
|
30
22
|
|
@@ -32,14 +24,13 @@ module Timber
|
|
32
24
|
@lock.synchronize do
|
33
25
|
old = @array
|
34
26
|
@array = []
|
35
|
-
@bytesize = 0
|
36
27
|
return old
|
37
28
|
end
|
38
29
|
end
|
39
30
|
|
40
31
|
def full?
|
41
32
|
@lock.synchronize do
|
42
|
-
|
33
|
+
size >= @max_size
|
43
34
|
end
|
44
35
|
end
|
45
36
|
|
@@ -70,7 +61,7 @@ module Timber
|
|
70
61
|
end
|
71
62
|
|
72
63
|
TIMBER_URL = "https://logs.timber.io/frames".freeze
|
73
|
-
CONTENT_TYPE = "application/
|
64
|
+
CONTENT_TYPE = "application/msgpack".freeze
|
74
65
|
USER_AGENT = "Timber Ruby Gem/#{Timber::VERSION}".freeze
|
75
66
|
DELIVERY_FREQUENCY_SECONDS = 2.freeze
|
76
67
|
RETRY_LIMIT = 5.freeze
|
@@ -90,17 +81,17 @@ module Timber
|
|
90
81
|
# @param api_key [String] The API key provided to you after you add your application to
|
91
82
|
# [Timber](https://timber.io).
|
92
83
|
# @param [Hash] options the options to create a HTTP log device with.
|
93
|
-
# @option attributes [Symbol] :
|
94
|
-
#
|
95
|
-
# @option attributes [Symbol] :debug Whether to print debug output or not. This is also
|
84
|
+
# @option attributes [Symbol] :batch_size (500) Determines the maximum of log lines in each HTTP
|
85
|
+
# payload. If the queue exceeds this limit a HTTP request will be issued.
|
86
|
+
# @option attributes [Symbol] :debug (false) Whether to print debug output or not. This is also
|
96
87
|
# inferred from ENV['debug']. Output will be sent to `Timber::Config.logger`.
|
97
88
|
# @option attributes [Symbol] :flush_interval (2) How often the client should
|
98
89
|
# attempt to deliver logs to the Timber API. The HTTP client buffers logs and this
|
99
90
|
# 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
|
91
|
+
# @option attributes [Symbol] :requests_per_conn (1000) The number of requests to send over a
|
101
92
|
# single persistent connection. After this number is met, the connection will be closed
|
102
93
|
# and a new one will be opened.
|
103
|
-
# @option attributes [Symbol] :request_queue The request queue object that queues Net::HTTP
|
94
|
+
# @option attributes [Symbol] :request_queue (SizedQueue.new(3)) The request queue object that queues Net::HTTP
|
104
95
|
# requests for delivery. By deafult this is a `SizedQueue` of size `3`. Meaning once
|
105
96
|
# 3 requests are placed on the queue for delivery, back pressure will be applied. IF
|
106
97
|
# you'd prefer to drop messages instead, pass a {DroppingSizedQueue}. See examples for
|
@@ -119,12 +110,12 @@ module Timber
|
|
119
110
|
@api_key = api_key
|
120
111
|
@debug = options[:debug] || ENV['debug']
|
121
112
|
@timber_url = URI.parse(options[:timber_url] || ENV['TIMBER_URL'] || TIMBER_URL)
|
122
|
-
@
|
113
|
+
@batch_size = options[:batch_size] || 500
|
123
114
|
@flush_interval = options[:flush_interval] || 2 # 2 seconds
|
124
115
|
@requests_per_conn = options[:requests_per_conn] || 1_000
|
125
|
-
@msg_queue = LogMsgQueue.new(@
|
116
|
+
@msg_queue = LogMsgQueue.new(@batch_size)
|
126
117
|
@request_queue = options[:request_queue] || SizedQueue.new(3)
|
127
|
-
@
|
118
|
+
@requests_in_flight = 0
|
128
119
|
|
129
120
|
if options[:threads] != false
|
130
121
|
@outlet_thread = Thread.new { outlet }
|
@@ -158,17 +149,12 @@ module Timber
|
|
158
149
|
msgs = @msg_queue.flush
|
159
150
|
return if msgs.empty?
|
160
151
|
|
161
|
-
body = ""
|
162
|
-
msgs.each do |msg|
|
163
|
-
body << msg
|
164
|
-
end
|
165
|
-
|
166
152
|
req = Net::HTTP::Post.new(@timber_url.path)
|
167
153
|
req['Accept'] = "application/json"
|
168
154
|
req['Authorization'] = authorization_payload
|
169
155
|
req['Content-Type'] = CONTENT_TYPE
|
170
156
|
req['User-Agent'] = USER_AGENT
|
171
|
-
req.body =
|
157
|
+
req.body = msgs.to_msgpack
|
172
158
|
@request_queue.enq(req)
|
173
159
|
@last_flush = Time.now
|
174
160
|
end
|
@@ -205,9 +191,9 @@ module Timber
|
|
205
191
|
http.start do |conn|
|
206
192
|
num_reqs = 0
|
207
193
|
while num_reqs < @requests_per_conn
|
208
|
-
#Blocks waiting for a request.
|
194
|
+
# Blocks waiting for a request.
|
209
195
|
req = @request_queue.deq
|
210
|
-
@
|
196
|
+
@requests_in_flight += 1
|
211
197
|
resp = nil
|
212
198
|
begin
|
213
199
|
resp = conn.request(req)
|
@@ -215,7 +201,7 @@ module Timber
|
|
215
201
|
logger.error("Timber request error: #{e.message}") if debug?
|
216
202
|
next
|
217
203
|
ensure
|
218
|
-
@
|
204
|
+
@requests_in_flight -= 1
|
219
205
|
end
|
220
206
|
num_reqs += 1
|
221
207
|
logger.info("Timber request successful: #{resp.code}") if debug?
|
data/lib/timber/log_entry.rb
CHANGED
data/lib/timber/logger.rb
CHANGED
@@ -119,31 +119,14 @@ module Timber
|
|
119
119
|
end
|
120
120
|
end
|
121
121
|
|
122
|
-
#
|
123
|
-
#
|
124
|
-
#
|
125
|
-
#
|
126
|
-
|
127
|
-
# Example message:
|
128
|
-
#
|
129
|
-
# {"level":"info","dt":"2016-09-01T07:00:00.000000-05:00","message":"My log message"}
|
130
|
-
#
|
131
|
-
class MsgPackFormatter < Formatter
|
122
|
+
# Passes through the LogEntry object. This is specifically used for the {Timber::LogDevices::HTTP}
|
123
|
+
# class. This allows the IO device to format it however it wants. This is neccessary for
|
124
|
+
# MessagePack because it requires a fixed array size before encoding. And since HTTP is
|
125
|
+
# sending data in batches, the encoding should happen there.
|
126
|
+
class PassThroughFormatter < Formatter
|
132
127
|
def call(severity, time, progname, msg)
|
133
|
-
|
134
|
-
hash = build_log_entry(severity, time, progname, msg).as_json
|
135
|
-
to_msgpack(hash) << "\n"
|
128
|
+
build_log_entry(severity, time, progname, msg)
|
136
129
|
end
|
137
|
-
|
138
|
-
private
|
139
|
-
def to_msgpack(msg)
|
140
|
-
begin
|
141
|
-
msg.to_msgpack
|
142
|
-
rescue NoMethodError
|
143
|
-
json = JSON.generate(msg)
|
144
|
-
JSON.parse(json).to_msgpack
|
145
|
-
end
|
146
|
-
end
|
147
130
|
end
|
148
131
|
|
149
132
|
# Creates a new Timber::Logger instances. Accepts the same arguments as `::Logger.new`.
|
@@ -156,7 +139,7 @@ module Timber
|
|
156
139
|
def initialize(*args)
|
157
140
|
super(*args)
|
158
141
|
if args.size == 1 and args.first.is_a?(LogDevices::HTTP)
|
159
|
-
self.formatter =
|
142
|
+
self.formatter = PassThroughFormatter.new
|
160
143
|
else
|
161
144
|
self.formatter = HybridFormatter.new
|
162
145
|
end
|
@@ -165,7 +148,7 @@ module Timber
|
|
165
148
|
def formatter=(value)
|
166
149
|
if @dev.is_a?(Timber::LogDevices::HTTP)
|
167
150
|
raise ArgumentError.new("The formatter cannot be changed when using the " +
|
168
|
-
"Timber::LogDevices::HTTP log device. The
|
151
|
+
"Timber::LogDevices::HTTP log device. The PassThroughFormatter must be used for proper " +
|
169
152
|
"delivery.")
|
170
153
|
end
|
171
154
|
super
|
data/lib/timber/version.rb
CHANGED
@@ -20,12 +20,11 @@ describe Timber::LogDevices::HTTP do
|
|
20
20
|
expect(msg_queue.flush).to eq(["test log message"])
|
21
21
|
end
|
22
22
|
|
23
|
-
context "with a low batch
|
24
|
-
let(:http) { described_class.new("MYKEY", :
|
23
|
+
context "with a low batch size" do
|
24
|
+
let(:http) { described_class.new("MYKEY", :batch_size => 2) }
|
25
25
|
|
26
|
-
it "should attempt a delivery when the
|
27
|
-
|
28
|
-
http.write(message)
|
26
|
+
it "should attempt a delivery when the limit is exceeded" do
|
27
|
+
http.write("test")
|
29
28
|
expect(http).to receive(:flush).exactly(1).times
|
30
29
|
http.write("my log message")
|
31
30
|
end
|
@@ -55,18 +54,30 @@ describe Timber::LogDevices::HTTP do
|
|
55
54
|
|
56
55
|
# Testing a private method because it helps break down our tests
|
57
56
|
describe "#flush" do
|
57
|
+
let(:time) { Time.utc(2016, 9, 1, 12, 0, 0) }
|
58
|
+
|
58
59
|
it "should add a request to the queue" do
|
59
60
|
http = described_class.new("MYKEY", threads: false)
|
60
|
-
|
61
|
+
log_entry = Timber::LogEntry.new("INFO", time, nil, "test log message 1", nil, nil)
|
62
|
+
http.write(log_entry)
|
63
|
+
log_entry = Timber::LogEntry.new("INFO", time, nil, "test log message 2", nil, nil)
|
64
|
+
http.write(log_entry)
|
61
65
|
http.send(:flush)
|
62
66
|
request_queue = http.instance_variable_get(:@request_queue)
|
63
67
|
request = request_queue.deq
|
64
68
|
expect(request).to be_kind_of(Net::HTTP::Post)
|
65
|
-
expect(request.body).to eq("
|
69
|
+
expect(request.body).to eq("\x92\x83\xA5level\xA4INFO\xA2dt\xBB2016-09-01T12:00:00.000000Z\xA7message\xB2test log message 1\x83\xA5level\xA4INFO\xA2dt\xBB2016-09-01T12:00:00.000000Z\xA7message\xB2test log message 2".force_encoding("ASCII-8BIT"))
|
66
70
|
|
67
71
|
message_queue = http.instance_variable_get(:@msg_queue)
|
68
72
|
expect(message_queue.size).to eq(0)
|
69
73
|
end
|
74
|
+
|
75
|
+
it "should preserve formatting for mshpack payloads" do
|
76
|
+
http = described_class.new("MYKEY", threads: false)
|
77
|
+
http.write("This is a log message 1".to_msgpack)
|
78
|
+
http.write("This is a log message 2".to_msgpack)
|
79
|
+
http.send(:flush)
|
80
|
+
end
|
70
81
|
end
|
71
82
|
|
72
83
|
# Testing a private method because it helps break down our tests
|
@@ -74,30 +85,34 @@ describe Timber::LogDevices::HTTP do
|
|
74
85
|
it "should start a intervaled flush thread and flush on an interval" do
|
75
86
|
http = described_class.new("MYKEY", flush_interval: 0.1)
|
76
87
|
expect(http).to receive(:flush).exactly(1).times
|
77
|
-
sleep 0.
|
88
|
+
sleep 0.12 # too fast!
|
78
89
|
mock = expect(http).to receive(:flush).exactly(1).times
|
79
|
-
sleep 0.
|
90
|
+
sleep 0.12 # too fast!
|
80
91
|
end
|
81
92
|
end
|
82
93
|
|
83
94
|
# Outlet
|
84
95
|
describe "#outlet" do
|
96
|
+
let(:time) { Time.utc(2016, 9, 1, 12, 0, 0) }
|
97
|
+
|
85
98
|
it "should start a intervaled flush thread and flush on an interval" do
|
86
99
|
stub = stub_request(:post, "https://logs.timber.io/frames").
|
87
100
|
with(
|
88
|
-
:body => "
|
101
|
+
:body => "\x92\x83\xA5level\xA4INFO\xA2dt\xBB2016-09-01T12:00:00.000000Z\xA7message\xB2test log message 1\x83\xA5level\xA4INFO\xA2dt\xBB2016-09-01T12:00:00.000000Z\xA7message\xB2test log message 2".force_encoding("ASCII-8BIT"),
|
89
102
|
:headers => {
|
90
103
|
'Accept' => 'application/json',
|
91
104
|
'Authorization' => 'Basic TVlLRVk=',
|
92
|
-
'Content-Type' => 'application/
|
105
|
+
'Content-Type' => 'application/msgpack',
|
93
106
|
'User-Agent' => "Timber Ruby Gem/#{Timber::VERSION}"
|
94
107
|
}
|
95
108
|
).
|
96
109
|
to_return(:status => 200, :body => "", :headers => {})
|
97
110
|
|
98
111
|
http = described_class.new("MYKEY", flush_interval: 0.1)
|
99
|
-
|
100
|
-
http.write(
|
112
|
+
log_entry = Timber::LogEntry.new("INFO", time, nil, "test log message 1", nil, nil)
|
113
|
+
http.write(log_entry)
|
114
|
+
log_entry = Timber::LogEntry.new("INFO", time, nil, "test log message 2", nil, nil)
|
115
|
+
http.write(log_entry)
|
101
116
|
sleep 0.3
|
102
117
|
|
103
118
|
expect(stub).to have_been_requested.times(1)
|
data/spec/timber/logger_spec.rb
CHANGED
@@ -93,32 +93,8 @@ describe Timber::Logger, :rails_23 => true do
|
|
93
93
|
context "with the HTTP log device" do
|
94
94
|
let(:io) { Timber::LogDevices::HTTP.new("my_key") }
|
95
95
|
|
96
|
-
it "should use the
|
97
|
-
expect(logger.formatter).to be_kind_of(Timber::Logger::
|
98
|
-
end
|
99
|
-
|
100
|
-
it "should log properly with the msgpack format" do
|
101
|
-
event = Timber::Events::Custom.new(
|
102
|
-
type: :payment_rejected,
|
103
|
-
message: "Payment rejected",
|
104
|
-
data: {customer_id: "abcd1234", amount: 100}
|
105
|
-
)
|
106
|
-
expect(io).to receive(:write).exactly(1).times
|
107
|
-
logger.error(event)
|
108
|
-
end
|
109
|
-
end
|
110
|
-
end
|
111
|
-
|
112
|
-
describe described_class::MsgPackFormatter do
|
113
|
-
describe "#call" do
|
114
|
-
let(:time) { Time.utc(2016, 9, 1, 12, 0, 0) }
|
115
|
-
|
116
|
-
it "should produce a message pack formatted message" do
|
117
|
-
formatter = described_class.new
|
118
|
-
result = formatter.call("INFO", time, nil, "this is a test message")
|
119
|
-
hash = {"level"=>"info", "dt"=>"2016-09-01T12:00:00.000000Z", "message"=>"this is a test message"}
|
120
|
-
expected = hash.to_msgpack + "\n"
|
121
|
-
expect(result).to eq(expected)
|
96
|
+
it "should use the PassThroughFormatter" do
|
97
|
+
expect(logger.formatter).to be_kind_of(Timber::Logger::PassThroughFormatter)
|
122
98
|
end
|
123
99
|
end
|
124
100
|
end
|