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