loggability 0.15.1 → 0.16.0

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.
@@ -24,36 +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 +target+ (a
32
- ### object that responds to #>>).
33
- def initialize( target )
34
- @target = target
35
- end
36
-
37
-
38
- ######
39
- public
40
- ######
41
-
42
- # The target of the log device
43
- attr_reader :target
44
-
45
-
46
- ### Append the specified +message+ to the target.
47
- def write( message )
48
- @target << message
49
- end
50
-
51
- ### No-op -- this is here just so Logger doesn't complain
52
- def close; end
53
-
54
- end # class AppendingLogDevice
55
-
56
-
57
27
  # Proxy for the Logger that injects the name of the object it wraps as the 'progname'
58
28
  # of each log message.
59
29
  class ObjectNameProxy
@@ -273,14 +243,17 @@ class Loggability::Logger < ::Logger
273
243
  ### logging to IO objects and files (given a filename in a String), this method can also
274
244
  ### set up logging to any object that responds to #<<.
275
245
  def output_to( target, *args )
276
- if target.is_a?( Logger::LogDevice ) ||
277
- target.is_a?( Loggability::Logger::AppendingLogDevice )
246
+ if target.is_a?( Logger::LogDevice ) || target.is_a?( Loggability::LogDevice )
278
247
  self.logdev = target
279
248
  elsif target.respond_to?( :write ) || target.is_a?( String )
280
249
  opts = { :shift_age => args.shift || 0, :shift_size => args.shift || 1048576 }
281
250
  self.logdev = Logger::LogDevice.new( target, **opts )
251
+ elsif target.respond_to?( :any? ) && target.any?( Loggability::LogDevice )
252
+ self.logdev = MultiDevice.new( target )
282
253
  elsif target.respond_to?( :<< )
283
- 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 )
284
257
  else
285
258
  raise ArgumentError, "don't know how to output to %p (a %p)" % [ target, target.class ]
286
259
  end
@@ -311,10 +284,11 @@ class Loggability::Logger < ::Logger
311
284
  end
312
285
 
313
286
 
314
- ### Set a new +formatter+ for the logger. If +formatter+ is +nil+ or +:default+, this causes the
315
- ### logger to fall back to its default formatter. If it's a Symbol other than +:default+, it looks
316
- ### for a similarly-named formatter under loggability/formatter/ and uses that. If +formatter+ is
317
- ### an object that responds to #call (e.g., a Proc or a Method object), that object is used directly.
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.
318
292
  ###
319
293
  ### Procs and methods should have the method signature: (severity, datetime, progname, msg).
320
294
  ###
data/spec/helpers.rb CHANGED
@@ -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
 
@@ -106,5 +105,6 @@ RSpec.configure do |c|
106
105
  c.include( Loggability::SpecHelpers )
107
106
  c.filter_run_excluding( :configurability ) unless defined?( Configurability )
108
107
 
108
+
109
109
  end
110
110
 
@@ -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,148 @@
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
+ end
@@ -106,6 +106,7 @@ describe Loggability::Logger do
106
106
  expect( logger.level ).to eq( :warn )
107
107
  end
108
108
 
109
+
109
110
  it "defaults to :debug level when $DEBUG is true" do
110
111
  begin
111
112
  $DEBUG = true
@@ -115,6 +116,7 @@ describe Loggability::Logger do
115
116
  end
116
117
  end
117
118
 
119
+
118
120
  it "allows its levels to be set with integers like Logger" do
119
121
  newlevel = Logger::DEBUG
120
122
  $stderr.puts "Setting newlevel to %p" % [ newlevel ]
@@ -122,11 +124,13 @@ describe Loggability::Logger do
122
124
  expect( logger.level ).to eq( :debug )
123
125
  end
124
126
 
127
+
125
128
  it "allows its levels to be set with Symbolic level names" do
126
129
  logger.level = :info
127
130
  expect( logger.level ).to eq( :info )
128
131
  end
129
132
 
133
+
130
134
  it "allows its levels to be set with Stringish level names" do
131
135
  logger.level = 'fatal'
132
136
  expect( logger.level ).to eq( :fatal )
@@ -141,12 +145,14 @@ describe Loggability::Logger do
141
145
  expect( logger.logdev.dev ).to be( $stderr )
142
146
  end
143
147
 
148
+
144
149
  it "can be told to log to a file" do
145
150
  tmpfile = Tempfile.new( 'loggability-device-spec' )
146
151
  logger.output_to( tmpfile.path )
147
152
  expect( logger.logdev.dev ).to be_a( File )
148
153
  end
149
154
 
155
+
150
156
  it "supports log-rotation arguments for logfiles" do
151
157
  tmpfile = Tempfile.new( 'loggability-device-spec' )
152
158
  logger.output_to( tmpfile.path, 5, 125000 )
@@ -156,16 +162,45 @@ describe Loggability::Logger do
156
162
  expect( logger.logdev.instance_variable_get(:@shift_size) ).to eq( 125000 )
157
163
  end
158
164
 
165
+
166
+ it "can be told to log to a file and delegate to ruby's built-in logger log device" do
167
+ logfile = double( "logfile.log" )
168
+ expect( Loggability::LogDevice ).to receive( :create ).
169
+ with( :file, 'log_file.log' ).
170
+ and_return( logfile )
171
+
172
+ logfile = Loggability::LogDevice.create( :file, 'log_file.log' )
173
+ expect( logger ).to receive( :output_to ).
174
+ with( logfile ).
175
+ and_return( nil )
176
+
177
+ logger.output_to( logfile )
178
+
179
+ expect( logger.logdev ).to be_a( Logger::LogDevice )
180
+ end
181
+
182
+
183
+ it "can be told to log to a custom log device type" do
184
+ logger.output_to( :http, 'https://logapi.example.com:41133/v1/logintake' )
185
+
186
+ device = logger.logdev
187
+
188
+ expect( device ).to be_a( Loggability::LogDevice::Http )
189
+ expect( device.endpoint.to_s ).to eq( 'https://logapi.example.com:41133/v1/logintake' )
190
+ end
191
+
192
+
159
193
  it "can be told to log to an Array" do
160
194
  logmessages = []
161
195
  logger.output_to( logmessages )
162
- expect( logger.logdev ).to be_a( Loggability::Logger::AppendingLogDevice )
196
+ expect( logger.logdev ).to be_a( Loggability::LogDevice::Appending )
163
197
  logger.level = :debug
164
198
  logger.info( "Something happened." )
165
199
  expect( logmessages.size ).to eq( 1 )
166
200
  expect( logmessages.first ).to match( /something happened/i )
167
201
  end
168
202
 
203
+
169
204
  it "doesn't re-wrap a Logger::LogDevice" do
170
205
  tmpfile = Tempfile.new( 'loggability-device-spec' )
171
206
  logger.output_to( tmpfile.path, 5, 125000 )
@@ -176,7 +211,8 @@ describe Loggability::Logger do
176
211
  expect( logger.logdev ).to be( original_logdev )
177
212
  end
178
213
 
179
- it "doesn't re-wrap an AppendingLogDevice" do
214
+
215
+ it "doesn't re-wrap an Appending log device" do
180
216
  log_array = []
181
217
  logger.output_to( log_array )
182
218
  logger.output_to( logger.logdev )
@@ -193,11 +229,13 @@ describe Loggability::Logger do
193
229
  expect( logger.formatter ).to be_a( Loggability::Formatter::Default )
194
230
  end
195
231
 
232
+
196
233
  it "can be told to use the default formatter explicitly" do
197
234
  logger.format_as( :default )
198
235
  expect( logger.formatter ).to be_a( Loggability::Formatter::Default )
199
236
  end
200
237
 
238
+
201
239
  it "can be told to use a block as a formatter" do
202
240
  logger.format_with do |severity, datetime, progname, msg|
203
241
  original_formatter.call(severity, datetime, progname, msg.dump)
@@ -206,11 +244,13 @@ describe Loggability::Logger do
206
244
  expect( logger.formatter ).to be_a( Proc )
207
245
  end
208
246
 
247
+
209
248
  it "can be told to use the HTML formatter" do
210
249
  logger.format_as( :html )
211
250
  expect( logger.formatter ).to be_a( Loggability::Formatter::HTML )
212
251
  end
213
252
 
253
+
214
254
  it "supports formatting with ::Logger::Formatter, too" do
215
255
  output = []
216
256
  logger.output_to( output )
@@ -244,6 +284,7 @@ describe Loggability::Logger do
244
284
  expect( messages.first ).to match( expected_format )
245
285
  end
246
286
 
287
+
247
288
  it "has a terse inspection format" do
248
289
  object = Object.new
249
290
  expect(