loggability 0.14.0 → 0.18.0

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