loggability 0.15.0.pre20190714094638 → 0.18.1

Sign up to get free protection for your applications and to get access to all the features.
@@ -24,35 +24,6 @@ class Loggability::Logger < ::Logger
24
24
  DEFAULT_SHIFT_SIZE = 1048576
25
25
 
26
26
 
27
- # A log device that appends to the object it's constructed with instead of writing
28
- # to a file descriptor or a file.
29
- class AppendingLogDevice
30
-
31
- ### Create a new AppendingLogDevice that will append content to +array+.
32
- def initialize( target )
33
- @target = target
34
- end
35
-
36
-
37
- ######
38
- public
39
- ######
40
-
41
- # The target of the log device
42
- attr_reader :target
43
-
44
-
45
- ### Append the specified +message+ to the target.
46
- def write( message )
47
- @target << message
48
- end
49
-
50
- ### No-op -- this is here just so Logger doesn't complain
51
- def close; end
52
-
53
- end # class AppendingLogDevice
54
-
55
-
56
27
  # Proxy for the Logger that injects the name of the object it wraps as the 'progname'
57
28
  # of each log message.
58
29
  class ObjectNameProxy
@@ -182,6 +153,28 @@ class Loggability::Logger < ::Logger
182
153
  end
183
154
 
184
155
 
156
+ ### Log a message if the +severity+ is high enough. Overridden to account for
157
+ ### the overridden #level.
158
+ def add( severity, message=nil, progname=nil )
159
+ return true if severity < self.sev_threshold
160
+ progname ||= @progname
161
+
162
+ unless message
163
+ if block_given?
164
+ message = yield
165
+ else
166
+ message = progname
167
+ progname = @progname
168
+ end
169
+ end
170
+
171
+ msg = self.format_message( self.format_severity(severity), Time.now, progname, message )
172
+ self.logdev.write( msg )
173
+
174
+ return true
175
+ end
176
+
177
+
185
178
  ### Append operator -- Override Logger's append so log messages always have
186
179
  ### formatting, and are always appended at :debug level.
187
180
  def <<( message )
@@ -224,8 +217,7 @@ class Loggability::Logger < ::Logger
224
217
 
225
218
  ### Return the logger's level as a Symbol.
226
219
  def level
227
- numeric_level = super
228
- return LOG_LEVEL_NAMES[ numeric_level ]
220
+ return LOG_LEVEL_NAMES[ self.sev_threshold ]
229
221
  end
230
222
 
231
223
 
@@ -251,14 +243,17 @@ class Loggability::Logger < ::Logger
251
243
  ### logging to IO objects and files (given a filename in a String), this method can also
252
244
  ### set up logging to any object that responds to #<<.
253
245
  def output_to( target, *args )
254
- if target.is_a?( Logger::LogDevice ) ||
255
- target.is_a?( Loggability::Logger::AppendingLogDevice )
246
+ if target.is_a?( Logger::LogDevice ) || target.is_a?( Loggability::LogDevice )
256
247
  self.logdev = target
257
248
  elsif target.respond_to?( :write ) || target.is_a?( String )
258
249
  opts = { :shift_age => args.shift || 0, :shift_size => args.shift || 1048576 }
259
- self.logdev = Logger::LogDevice.new( target, opts )
250
+ self.logdev = Logger::LogDevice.new( target, **opts )
251
+ elsif target.respond_to?( :any? ) && target.any?( Loggability::LogDevice )
252
+ self.logdev = MultiDevice.new( target )
260
253
  elsif target.respond_to?( :<< )
261
- self.logdev = AppendingLogDevice.new( target )
254
+ self.logdev = Loggability::LogDevice.create( :appending, target )
255
+ elsif target.is_a?( Symbol )
256
+ self.logdev = Loggability::LogDevice.create( target, *args )
262
257
  else
263
258
  raise ArgumentError, "don't know how to output to %p (a %p)" % [ target, target.class ]
264
259
  end
@@ -282,10 +277,18 @@ class Loggability::Logger < ::Logger
282
277
  end
283
278
 
284
279
 
285
- ### Set a new +formatter+ for the logger. If +formatter+ is +nil+ or +:default+, this causes the
286
- ### logger to fall back to its default formatter. If it's a Symbol other than +:default+, it looks
287
- ### for a similarly-named formatter under loggability/formatter/ and uses that. If +formatter+ is
288
- ### an object that responds to #call (e.g., a Proc or a Method object), that object is used directly.
280
+ ### Return the formatted name of the given +severity+.
281
+ def format_severity( severity )
282
+ name = LOG_LEVEL_NAMES[ severity ] || severity.to_s
283
+ return name.upcase
284
+ end
285
+
286
+
287
+ ### Set a new +formatter+ for the logger. If +formatter+ is +nil+ or +:default+, this causes
288
+ ### the logger to fall back to its default formatter. If it's a Symbol other than +:default+,
289
+ ### it looks for a similarly-named formatter under loggability/formatter/ and uses that. If
290
+ ### +formatter+ is an object that responds to #call (e.g., a Proc or a Method object), that
291
+ ### object is used directly.
289
292
  ###
290
293
  ### Procs and methods should have the method signature: (severity, datetime, progname, msg).
291
294
  ###
@@ -20,7 +20,6 @@ require 'timecop'
20
20
  require 'loggability'
21
21
  require 'loggability/spechelpers'
22
22
 
23
-
24
23
  # Helpers specific to Loggability specs
25
24
  module SpecHelpers
26
25
 
@@ -99,10 +98,13 @@ RSpec.configure do |c|
99
98
  c.mock_with( :rspec ) do |mock|
100
99
  mock.syntax = :expect
101
100
  end
101
+ c.warnings = true
102
+ c.profile_examples = 5
102
103
 
103
104
  c.include( SpecHelpers )
104
105
  c.include( Loggability::SpecHelpers )
105
106
  c.filter_run_excluding( :configurability ) unless defined?( Configurability )
106
107
 
108
+
107
109
  end
108
110
 
@@ -0,0 +1,50 @@
1
+ # -*- ruby -*-
2
+ # vim: set nosta noet ts=4 sw=4:
3
+ # frozen_string_literal: true
4
+
5
+ require_relative '../../helpers'
6
+
7
+ require 'loggability/formatter/default'
8
+
9
+
10
+ describe Loggability::Formatter::Default do
11
+
12
+ it "formats messages with the pattern it's constructed with" do
13
+ formatter = described_class.new( '[%5$s] %7$s' )
14
+ result = formatter.call( 'INFO', Time.at(1336286481), nil, 'Foom.' )
15
+ expect( result ).to match(/\[INFO\] Foom./i)
16
+ end
17
+
18
+
19
+ it "formats exceptions into useful messages" do
20
+ formatter = described_class.new( '[%5$s] %7$s' )
21
+ msg = nil
22
+
23
+ begin
24
+ raise ArgumentError, "invalid argument"
25
+ rescue => err
26
+ msg = formatter.call( 'INFO', Time.at(1336286481), nil, err )
27
+ end
28
+
29
+ expect( msg ).to match(/\[INFO\] ArgumentError: invalid argument/i)
30
+ end
31
+
32
+
33
+ it "formats regular objects into useful messages" do
34
+ formatter = described_class.new( '[%5$s] %7$s' )
35
+ result = formatter.call( 'INFO', Time.at(1336286481), nil, Object.new )
36
+
37
+ expect( result ).to match(/\[INFO\] #<Object:0x[[:xdigit:]]+>/i)
38
+ end
39
+
40
+
41
+ it "includes the thread ID if logging from a thread other than the main thread" do
42
+ formatter = described_class.new( '%4$d' )
43
+ thr = Thread.new do
44
+ formatter.call( 'INFO', Time.now, nil, 'Foom.' )
45
+ end
46
+ expect( thr.value ).to eq( thr.object_id.to_s )
47
+ end
48
+
49
+ end
50
+
@@ -0,0 +1,61 @@
1
+ # -*- ruby -*-
2
+ # vim: set nosta noet ts=4 sw=4:
3
+ # frozen_string_literal: true
4
+
5
+ require_relative '../../helpers'
6
+
7
+ require 'loggability/formatter/structured'
8
+
9
+
10
+ describe Loggability::Formatter::Structured do
11
+
12
+ before( :each ) do
13
+ ENV['TZ'] = 'UTC'
14
+ end
15
+
16
+
17
+ it "outputs a stream of JSON objects" do
18
+ expect(
19
+ subject.call('INFO', Time.at(1336286481), nil, "Foom.")
20
+ ).to eq(
21
+ %q|{"@version":1,"@timestamp":"2012-05-06T06:41:21.000+00:00"| +
22
+ %q|,"level":"INFO","progname":null,"message":"Foom."}|
23
+ )
24
+ end
25
+
26
+
27
+ it "includes a time even if called without one" do
28
+ Timecop.freeze( Time.at(1563114765.123) ) do
29
+ expect(
30
+ subject.call('WARN', nil, nil, "Right.")
31
+ ).to match( %r(
32
+ \{
33
+ "@version":1,
34
+ "@timestamp":"2019-07-14T14:32:45\.\d{3}\+00:00",
35
+ "level":"WARN",
36
+ "progname":null,
37
+ "message":"Right\."
38
+ \}
39
+ )x )
40
+ end
41
+ end
42
+
43
+
44
+ it "defaults to DEBUG severity" do
45
+ Timecop.freeze( Time.at(1563114765.123) ) do
46
+ expect(
47
+ subject.call(nil, nil, nil, "Crane.")
48
+ ).to match( %r(
49
+ \{
50
+ "@version":1,
51
+ "@timestamp":"2019-07-14T14:32:45\.\d{3}\+00:00",
52
+ "level":"DEBUG",
53
+ "progname":null,
54
+ "message":"Crane\."
55
+ \}
56
+ )x )
57
+ end
58
+ end
59
+
60
+ end
61
+
@@ -0,0 +1,27 @@
1
+ # -*- rspec -*-
2
+ #encoding: utf-8
3
+
4
+ require 'rspec'
5
+
6
+ require 'loggability/logger'
7
+ require 'loggability/log_device/appending'
8
+
9
+
10
+ describe Loggability::LogDevice::Appending do
11
+
12
+ let ( :logger ) { described_class.new( [] ) }
13
+
14
+
15
+ it "The target is an array" do
16
+ expect( logger.target ).to be_instance_of( Array )
17
+ end
18
+
19
+
20
+ it "can append to the array" do
21
+ logger.write("log message one")
22
+ logger.write("log message two")
23
+ expect( logger.target.size ).to eq( 2 )
24
+ end
25
+
26
+ end
27
+
@@ -0,0 +1,67 @@
1
+ # -*- rspec -*-
2
+ #encoding: utf-8
3
+
4
+ require 'securerandom'
5
+ require 'rspec'
6
+
7
+ require 'loggability/logger'
8
+ require 'loggability/log_device/datadog'
9
+
10
+
11
+ describe Loggability::LogDevice::Datadog do
12
+
13
+
14
+ let( :api_key ) { SecureRandom.hex(24) }
15
+ let( :http_client ) { instance_double(Net::HTTP) }
16
+
17
+
18
+ it "includes the configured API key in request headers" do
19
+ device = described_class.new(
20
+ api_key,
21
+ max_batch_size: 3,
22
+ batch_interval: 0.1,
23
+ executor_class: Concurrent::ImmediateExecutor )
24
+ device.instance_variable_set( :@http_client, http_client )
25
+
26
+ expect( http_client ).to receive( :request ) do |request|
27
+ expect( request ).to be_a( Net::HTTP::Post )
28
+ expect( request['Content-type'] ).to match( %r|application/json|i )
29
+ expect( request['DD-API-KEY'] ).to eq( api_key )
30
+ end.at_least( :once )
31
+
32
+ device.write( "message data" * 10 ) # 120 bytes
33
+ device.write( "message data" * 100 ) # 1200 bytes
34
+ device.write( "message data" * 85 ) # 1020 bytes
35
+ device.write( "message data" * 86 ) # 1032 bytes
36
+
37
+ sleep( 0.1 ) until device.logs_queue.empty?
38
+ end
39
+
40
+
41
+ it "includes the hostname in individual log messages" do
42
+ device = described_class.new(
43
+ api_key,
44
+ max_batch_size: 3,
45
+ batch_interval: 0.1,
46
+ executor_class: Concurrent::ImmediateExecutor )
47
+ device.instance_variable_set( :@http_client, http_client )
48
+
49
+ expect( http_client ).to receive( :request ) do |request|
50
+ expect( request ).to be_a( Net::HTTP::Post )
51
+
52
+ data = JSON.parse( request.body )
53
+
54
+ expect( data ).to all( be_a Hash )
55
+ expect( data ).to all( include('hostname' => device.hostname) )
56
+ end.at_least( :once )
57
+
58
+ device.write( "message data" * 10 ) # 120 bytes
59
+ device.write( "message data" * 100 ) # 1200 bytes
60
+ device.write( "message data" * 85 ) # 1020 bytes
61
+ device.write( "message data" * 86 ) # 1032 bytes
62
+
63
+ sleep( 0.1 ) until device.logs_queue.empty?
64
+ end
65
+
66
+ end
67
+
@@ -0,0 +1,27 @@
1
+ # -*- rspec -*-
2
+ #encoding: utf-8
3
+
4
+ require 'tempfile'
5
+ require 'rspec'
6
+
7
+ require 'loggability/logger'
8
+ require 'loggability/log_device/file'
9
+
10
+
11
+ describe Loggability::LogDevice::File do
12
+
13
+ let( :logfile ) { Tempfile.new( 'test.log' ) }
14
+ let( :logger ) { described_class.new( logfile.path ) }
15
+
16
+
17
+ it "The logger is an instance of Loggability::LogDevice::File" do
18
+ expect( logger ).to be_instance_of( Loggability::LogDevice::File )
19
+ end
20
+
21
+
22
+ it "The log device is delegated to Ruby's built-in log device" do
23
+ expect( logger.target ).to be_instance_of( ::Logger::LogDevice )
24
+ end
25
+
26
+ end
27
+
@@ -0,0 +1,217 @@
1
+ # -*- rspec -*-
2
+
3
+ require_relative '../../helpers'
4
+
5
+ require 'tempfile'
6
+ require 'rspec'
7
+
8
+ require 'loggability/log_device/http'
9
+
10
+
11
+ describe Loggability::LogDevice::Http do
12
+
13
+
14
+ let( :http_client ) { instance_double(Net::HTTP) }
15
+
16
+
17
+ it "can be created with defaults" do
18
+ result = described_class.new
19
+
20
+ expect( result ).to be_an_instance_of( described_class )
21
+ expect( result.batch_interval ).to eq( described_class::DEFAULT_BATCH_INTERVAL )
22
+ expect( result.write_timeout ).to eq( described_class::DEFAULT_WRITE_TIMEOUT )
23
+ end
24
+
25
+
26
+ it "doesn't start when created" do
27
+ result = described_class.new
28
+
29
+ expect( result ).to_not be_running
30
+ end
31
+
32
+
33
+ it "sends logs when a full batch is ready" do
34
+ device = described_class.new( max_batch_size: 3, executor_class: Concurrent::ImmediateExecutor )
35
+ device.instance_variable_set( :@http_client, http_client )
36
+
37
+ expect( http_client ).to receive( :request ) do |request|
38
+ expect( request ).to be_a( Net::HTTP::Post )
39
+ expect( request['Content-type'] ).to match( %r|application/json|i )
40
+ expect( request.body ).to match( /message 1/i )
41
+ expect( request.body ).to match( /message 2/i )
42
+ expect( request.body ).to match( /message 3/i )
43
+ end
44
+
45
+ device.write( "Message 1" )
46
+ device.write( "Message 2" )
47
+ device.write( "Message 3" )
48
+ device.write( "Message 4" )
49
+
50
+ expect( device.logs_queue ).to have_attributes( length: 1 )
51
+ end
52
+
53
+
54
+ it "sends logs when enough time has elapsed since the last message" do
55
+ device = described_class.new(
56
+ max_batch_size: 3, batch_interval: 0.1, executor_class: Concurrent::ImmediateExecutor )
57
+ device.instance_variable_set( :@http_client, http_client )
58
+ device.start
59
+ device.timer_task.shutdown # Don't let the timer fire
60
+
61
+ expect( http_client ).to receive( :request ) do |request|
62
+ expect( request ).to be_a( Net::HTTP::Post )
63
+ expect( request['Content-type'] ).to match( %r|application/json|i )
64
+ expect( request.body ).to match( /message 1/i )
65
+ expect( request.body ).to match( /message 2/i )
66
+ end
67
+
68
+ device.write( "Message 1" )
69
+
70
+ # Now wait for the batch interval to pass and send another
71
+ sleep device.batch_interval
72
+ expect( device ).to have_batch_ready
73
+ device.write( "Message 2" )
74
+
75
+ expect( device.logs_queue ).to have_attributes( length: 0 )
76
+ end
77
+
78
+
79
+ it "sends logs on the batch interval even when messages aren't arriving" do
80
+ device = described_class.new(
81
+ max_batch_size: 3, batch_interval: 0.1, executor_class: Concurrent::ImmediateExecutor )
82
+ device.instance_variable_set( :@http_client, http_client )
83
+
84
+ expect( http_client ).to receive( :request ) do |request|
85
+ expect( request ).to be_a( Net::HTTP::Post )
86
+ expect( request['Content-type'] ).to match( %r|application/json|i )
87
+ expect( request.body ).to match( /message 1/i )
88
+ end
89
+
90
+ device.write( "Message 1" )
91
+
92
+ # Now wait for the batch interval to pass and send another
93
+ sleep device.batch_interval * 2
94
+
95
+ expect( device.logs_queue ).to have_attributes( length: 0 )
96
+ end
97
+
98
+
99
+ it "limits messages to the configured byte size constraints" do
100
+ device = described_class.new(
101
+ max_batch_size: 3,
102
+ max_message_bytesize: 1024,
103
+ batch_interval: 0.1,
104
+ executor_class: Concurrent::ImmediateExecutor )
105
+ device.instance_variable_set( :@http_client, http_client )
106
+
107
+ expect( http_client ).to receive( :request ) do |request|
108
+ expect( request ).to be_a( Net::HTTP::Post )
109
+ expect( request['Content-type'] ).to match( %r|application/json|i )
110
+
111
+ data = JSON.parse( request.body )
112
+
113
+ expect( data ).to all( have_attributes(bytesize: a_value <= 1024) )
114
+ end.at_least( :once )
115
+
116
+ device.write( "message data" * 10 ) # 120 bytes
117
+ device.write( "message data" * 100 ) # 1200 bytes
118
+ device.write( "message data" * 85 ) # 1020 bytes
119
+ device.write( "message data" * 86 ) # 1032 bytes
120
+
121
+ sleep( 0.1 ) until device.logs_queue.empty?
122
+ end
123
+
124
+
125
+ it "limits the batch to the configured byte size constraints" do
126
+ device = described_class.new(
127
+ max_batch_bytesize: 1024,
128
+ batch_interval: 0.1,
129
+ executor_class: Concurrent::ImmediateExecutor )
130
+ device.instance_variable_set( :@http_client, http_client )
131
+
132
+ expect( http_client ).to receive( :request ) do |request|
133
+ expect( request ).to be_a( Net::HTTP::Post )
134
+ expect( request['Content-type'] ).to match( %r|application/json|i )
135
+
136
+ expect( request.body.bytesize ).to be <= 1024
137
+ end.at_least( :once )
138
+
139
+ 20.times { device.write( "message data" * 10 ) } # 120 bytes
140
+ 20.times { device.write( "message data" * 100 ) } # 1200 bytes
141
+ 20.times { device.write( "message data" * 85 ) } # 1020 bytes
142
+ 20.times { device.write( "message data" * 86 ) } # 1032 bytes
143
+
144
+ sleep( 0.1 ) until device.logs_queue.empty?
145
+ end
146
+
147
+
148
+ it "uses an HTTP client for the appropriate host and port" do
149
+ device = described_class.new(
150
+ 'http://logs.example.com:12881/v1/log_ingester',
151
+ executor_class: Concurrent::ImmediateExecutor )
152
+ http = device.http_client
153
+
154
+ expect( http.address ).to eq( 'logs.example.com' )
155
+ expect( http.port ).to eq( 12881 )
156
+ end
157
+
158
+
159
+ it "verifies the peer cert if sending to an HTTPS endpoint" do
160
+ device = described_class.new(
161
+ 'https://logs.example.com:12881/v1/log_ingester',
162
+ executor_class: Concurrent::ImmediateExecutor )
163
+ http = device.http_client
164
+
165
+ expect( http.use_ssl? ).to be_truthy
166
+ expect( http.verify_mode ).to eq( OpenSSL::SSL::VERIFY_PEER )
167
+ end
168
+
169
+
170
+ it "stops queuing more messages if max queue size is reached" do
171
+ device = described_class.new(
172
+ max_batch_bytesize: 1024,
173
+ batch_interval: 100,
174
+ max_queue_bytesize: 100,
175
+ executor_class: Concurrent::ImmediateExecutor )
176
+ device.instance_variable_set( :@http_client, http_client )
177
+
178
+ expect( device ).to receive( :send_logs ).at_least( :once )
179
+
180
+ msg = "test message"
181
+ device.write(msg)
182
+ expect( device.logs_queue_bytesize == msg.bytesize )
183
+
184
+ hash_msg = { message: "This is a test log message", tags: ["tag1", "tag2"] }
185
+ device.write( hash_msg )
186
+ previous_bytesize = device.logs_queue_bytesize - hash_msg.to_json.bytesize
187
+ expect( device.logs_queue_bytesize ).to eq( hash_msg.to_json.bytesize + previous_bytesize )
188
+
189
+ queue_current_bytesize = device.logs_queue_bytesize
190
+ hash_msg = { message: "This is a test log message", tags: ["tag1", "tag2"] }
191
+ device.write( hash_msg )
192
+ expect( device.logs_queue_bytesize ).to eq( queue_current_bytesize )
193
+ end
194
+
195
+
196
+ it "reduces the queue bytesize once messages are sent" do
197
+ device = described_class.new(
198
+ max_batch_bytesize: 1024,
199
+ batch_interval: 100,
200
+ max_queue_bytesize: 100,
201
+ executor_class: Concurrent::ImmediateExecutor )
202
+ device.instance_variable_set( :@http_client, http_client )
203
+
204
+ expect( device ).to receive( :send_logs ).at_least( :once )
205
+ msg = "test message"
206
+ device.write(msg)
207
+ expect( device.logs_queue_bytesize == msg.bytesize )
208
+
209
+ msg = "this is just a test message"
210
+ device.write( msg )
211
+ previous_bytesize = device.logs_queue_bytesize - msg.bytesize
212
+ expect( device.logs_queue_bytesize ).to eq( msg.bytesize + previous_bytesize )
213
+
214
+ expect { device.get_next_log_payload }.to change { device.logs_queue_bytesize }.to( 0 )
215
+ end
216
+
217
+ end