loggability 0.14.0 → 0.18.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.
Files changed (42) hide show
  1. checksums.yaml +5 -5
  2. checksums.yaml.gz.sig +0 -0
  3. data.tar.gz.sig +0 -0
  4. data/History.rdoc +55 -0
  5. data/Manifest.txt +15 -4
  6. data/{README.rdoc → README.md} +78 -71
  7. data/Rakefile +5 -77
  8. data/lib/loggability.rb +19 -27
  9. data/lib/loggability/constants.rb +2 -2
  10. data/lib/loggability/formatter.rb +13 -67
  11. data/lib/loggability/formatter/color.rb +2 -2
  12. data/lib/loggability/formatter/default.rb +69 -6
  13. data/lib/loggability/formatter/html.rb +5 -5
  14. data/lib/loggability/formatter/structured.rb +35 -0
  15. data/lib/loggability/log_device.rb +86 -0
  16. data/lib/loggability/log_device/appending.rb +34 -0
  17. data/lib/loggability/log_device/datadog.rb +90 -0
  18. data/lib/loggability/log_device/file.rb +37 -0
  19. data/lib/loggability/log_device/http.rb +310 -0
  20. data/lib/loggability/logclient.rb +2 -1
  21. data/lib/loggability/logger.rb +45 -42
  22. data/lib/loggability/loghost.rb +2 -1
  23. data/lib/loggability/override.rb +2 -1
  24. data/lib/loggability/spechelpers.rb +1 -1
  25. data/spec/helpers.rb +6 -12
  26. data/spec/loggability/formatter/color_spec.rb +3 -7
  27. data/spec/loggability/formatter/default_spec.rb +50 -0
  28. data/spec/loggability/formatter/html_spec.rb +7 -7
  29. data/spec/loggability/formatter/structured_spec.rb +61 -0
  30. data/spec/loggability/formatter_spec.rb +42 -29
  31. data/spec/loggability/log_device/appending_spec.rb +27 -0
  32. data/spec/loggability/log_device/datadog_spec.rb +67 -0
  33. data/spec/loggability/log_device/file_spec.rb +27 -0
  34. data/spec/loggability/log_device/http_spec.rb +217 -0
  35. data/spec/loggability/logger_spec.rb +46 -5
  36. data/spec/loggability/loghost_spec.rb +3 -1
  37. data/spec/loggability/override_spec.rb +18 -5
  38. data/spec/loggability/spechelpers_spec.rb +3 -4
  39. data/spec/loggability_spec.rb +16 -13
  40. metadata +77 -105
  41. metadata.gz.sig +0 -0
  42. data/ChangeLog +0 -621
@@ -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
@@ -1,12 +1,12 @@
1
- # -*- rspec -*-
1
+ # -*- ruby -*-
2
+ # vim: set nosta noet ts=4 sw=4:
3
+ # frozen_string_literal: true
2
4
 
3
5
  require_relative '../helpers'
4
6
 
5
7
  require 'tempfile'
6
- require 'rspec'
7
8
 
8
9
  require 'loggability/logger'
9
- require 'loggability/formatter'
10
10
  require 'loggability/formatter/default'
11
11
 
12
12
 
@@ -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(