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,34 @@
1
+ #!/usr/bin/env ruby
2
+ # vim: set nosta noet ts=4 sw=4:
3
+ # encoding: utf-8
4
+
5
+ require 'loggability/logger' unless defined?( Loggability::Logger )
6
+
7
+ # A log device that appends to the object it's constructed with instead of writing
8
+ # to a file descriptor or a file.
9
+ class Loggability::LogDevice::Appending < Loggability::LogDevice
10
+
11
+ ### Create a new +Appending+ log device that will append content to +array+.
12
+ def initialize( target )
13
+ @target = target || []
14
+ end
15
+
16
+
17
+ ######
18
+ public
19
+ ######
20
+
21
+ # The target of the log device
22
+ attr_reader :target
23
+
24
+
25
+ ### Append the specified +message+ to the target.
26
+ def write( message )
27
+ @target << message
28
+ end
29
+
30
+
31
+ ### No-op -- this is here just so Logger doesn't complain
32
+ def close; end
33
+
34
+ end # class Loggability::LogDevice::Appending
@@ -0,0 +1,90 @@
1
+ #!/usr/bin/env ruby
2
+ # vim: set nosta noet ts=4 sw=4:
3
+ # encoding: utf-8
4
+
5
+ require 'uri'
6
+ require 'socket'
7
+ require 'net/https'
8
+ require 'json'
9
+ require 'concurrent'
10
+ require 'loggability/logger' unless defined?( Loggability::Logger )
11
+
12
+ require 'loggability/log_device/http'
13
+
14
+
15
+ # A log device that sends logs to Datadog's HTTP endpoint
16
+ # for receiving logs
17
+ class Loggability::LogDevice::Datadog < Loggability::LogDevice::Http
18
+
19
+ ### Datadog's HTTP endpoint URL for sending logs to
20
+ DEFAULT_ENDPOINT = URI( "https://http-intake.logs.datadoghq.com/v1/input" )
21
+
22
+ ### The max number of messages that can be sent to datadog in a single payload
23
+ MAX_BATCH_SIZE = 480
24
+
25
+ ### The max size in bytes for a single message.
26
+ ### Limiting the message size to 200kB to leave room for other info such as
27
+ ### tags, metadata, etc.
28
+ ### DataDog's max size for a single log entry is 256kB
29
+ MAX_MESSAGE_BYTESIZE = 204_800
30
+
31
+ ### The max size in bytes of all messages in the batch.
32
+ ### Limiting the total messages size to 4MB to leave room for other info such as
33
+ ### tags, metadata, etc.
34
+ ### Datadog's max size for the entire payload is 5MB
35
+ MAX_BATCH_BYTESIZE = 4_194_304
36
+
37
+ # Override the default HTTP device options for sending logs to DD
38
+ DEFAULT_OPTIONS = {
39
+ max_batch_size: MAX_BATCH_SIZE,
40
+ max_message_bytesize: MAX_MESSAGE_BYTESIZE,
41
+ max_batch_bytesize: MAX_BATCH_BYTESIZE,
42
+ }
43
+
44
+
45
+ ### Create a new Datadog
46
+ def initialize( api_key, endpoint=DEFAULT_ENDPOINT, options={} )
47
+ if endpoint.is_a?( Hash )
48
+ options = endpoint
49
+ endpoint = DEFAULT_ENDPOINT
50
+ end
51
+
52
+ super( endpoint, options )
53
+
54
+ @api_key = api_key
55
+ @hostname = Socket.gethostname
56
+ end
57
+
58
+
59
+ ######
60
+ public
61
+ ######
62
+
63
+ ##
64
+ # The name of the current host
65
+ attr_reader :hostname
66
+
67
+ ##
68
+ # The configured Datadog API key
69
+ attr_reader :api_key
70
+
71
+
72
+ ### Format an individual log +message+ for Datadog.
73
+ def format_log_message( message )
74
+ return {
75
+ hostname: self.hostname,
76
+ message: message
77
+ }.to_json
78
+ end
79
+
80
+
81
+ ### Overridden to add the configured API key to the headers of each request.
82
+ def make_batch_request
83
+ request = super
84
+
85
+ request[ 'DD-API-KEY' ] = self.api_key
86
+
87
+ return request
88
+ end
89
+
90
+ end # class Loggability::LogDevice::Datadog
@@ -0,0 +1,37 @@
1
+ #!/usr/bin/env ruby
2
+ # vim: set nosta noet ts=4 sw=4:
3
+ # encoding: utf-8
4
+
5
+ require 'logger'
6
+ require 'loggability/logger' unless defined?( Loggability::Logger )
7
+
8
+ # A log device that delegates to the ruby's default Logger's file device and writes
9
+ # to a file descriptor or a file.
10
+ class Loggability::LogDevice::File < Loggability::LogDevice
11
+
12
+ ### Create a new +File+ device that will write to the file using the built-in ruby's +File+ log device
13
+ def initialize( target )
14
+ @target = ::Logger::LogDevice.new( target )
15
+ end
16
+
17
+
18
+ ######
19
+ public
20
+ ######
21
+
22
+ # The target of the log device
23
+ attr_reader :target
24
+
25
+
26
+ ### Append the specified +message+ to the target.
27
+ def write( message )
28
+ self.target.write( message )
29
+ end
30
+
31
+
32
+ ### close the file
33
+ def close
34
+ self.target.close
35
+ end
36
+
37
+ end # class Loggability::LogDevice::File
@@ -0,0 +1,310 @@
1
+ #!/usr/bin/env ruby
2
+ # vim: set nosta noet ts=4 sw=4:
3
+ # encoding: utf-8
4
+
5
+ require 'socket'
6
+ require 'uri'
7
+ require 'net/https'
8
+ require 'json'
9
+ require 'concurrent'
10
+ require 'loggability/logger' unless defined?( Loggability::Logger )
11
+
12
+ require 'loggability/log_device'
13
+
14
+ # This is the a generalized class that allows its subclasses to send log
15
+ # messages to HTTP endpoints asynchronously on a separate thread.
16
+ class Loggability::LogDevice::Http < Loggability::LogDevice
17
+
18
+ # The default HTTP endpoint URL to send logs to
19
+ DEFAULT_ENDPOINT = "http://localhost:12775/v1/logs"
20
+
21
+ # The default maximum number of messages that can be sent to the server in a single payload
22
+ DEFAULT_MAX_BATCH_SIZE = 100
23
+
24
+ # The default max size in bytes for a single message.
25
+ DEFAULT_MAX_MESSAGE_BYTESIZE = 2 * 16
26
+
27
+ # The default number of seconds between batches
28
+ DEFAULT_BATCH_INTERVAL = 60
29
+
30
+ # The default number of seconds to wait for data to be written before timing out
31
+ DEFAULT_WRITE_TIMEOUT = 15
32
+
33
+ # The default Executor class to use for asynchronous tasks
34
+ DEFAULT_EXECUTOR_CLASS = Concurrent::SingleThreadExecutor
35
+
36
+ # The default for the maximum bytesize of the queue (1 GB)
37
+ DEFAULT_MAX_QUEUE_BYTESIZE = ( 2 ** 10 ) * ( 2 ** 10 ) * ( 2 ** 10 )
38
+
39
+ # The default options for new instances
40
+ DEFAULT_OPTIONS = {
41
+ execution_interval: DEFAULT_BATCH_INTERVAL,
42
+ write_timeout: DEFAULT_WRITE_TIMEOUT,
43
+ max_batch_size: DEFAULT_MAX_BATCH_SIZE,
44
+ max_message_bytesize: DEFAULT_MAX_MESSAGE_BYTESIZE,
45
+ executor_class: DEFAULT_EXECUTOR_CLASS,
46
+ }
47
+
48
+
49
+ ### Initialize the HTTP log device to send to the specified +endpoint+ with the
50
+ ### given +options+. Valid options are:
51
+ ###
52
+ ### [:batch_interval]
53
+ ### Maximum number of seconds between batches
54
+ ### [:write_timeout]
55
+ ### How many seconds to wait for data to be written while sending a batch
56
+ ### [:max_batch_size]
57
+ ### The maximum number of messages that can be in a single batch
58
+ ### [:max_batch_bytesize]
59
+ ### The maximum number of bytes that can be in the payload of a single batch
60
+ ### [:max_message_bytesize]
61
+ ### The maximum number of bytes that can be in a single message
62
+ ### [:executor_class]
63
+ ### The Concurrent executor class to use for asynchronous tasks.
64
+ def initialize( endpoint=DEFAULT_ENDPOINT, opts={} )
65
+ if endpoint.is_a?( Hash )
66
+ opts = endpoint
67
+ endpoint = DEFAULT_ENDPOINT
68
+ end
69
+
70
+ opts = DEFAULT_OPTIONS.merge( opts )
71
+
72
+ @endpoint = URI( endpoint ).freeze
73
+ @logs_queue = Queue.new
74
+
75
+ @logs_queue_bytesize = 0
76
+ @max_queue_bytesize = opts[:max_queue_bytesize] || DEFAULT_MAX_QUEUE_BYTESIZE
77
+ @batch_interval = opts[:batch_interval] || DEFAULT_BATCH_INTERVAL
78
+ @write_timeout = opts[:write_timeout] || DEFAULT_WRITE_TIMEOUT
79
+ @max_batch_size = opts[:max_batch_size] || DEFAULT_MAX_BATCH_SIZE
80
+ @max_message_bytesize = opts[:max_message_bytesize] || DEFAULT_MAX_MESSAGE_BYTESIZE
81
+ @executor_class = opts[:executor_class] || DEFAULT_EXECUTOR_CLASS
82
+
83
+ @max_batch_bytesize = opts[:max_batch_bytesize] || @max_batch_size * @max_message_bytesize
84
+ @last_send_time = Concurrent.monotonic_time
85
+ end
86
+
87
+
88
+ ######
89
+ public
90
+ ######
91
+
92
+ ##
93
+ # The single thread pool executor
94
+ attr_reader :executor
95
+
96
+ ##
97
+ # The URI of the endpoint to send messages to
98
+ attr_reader :endpoint
99
+
100
+ ##
101
+ # The Queue that contains any log messages which have not yet been sent to the
102
+ # logging service.
103
+ attr_reader :logs_queue
104
+
105
+ ##
106
+ # The max bytesize of the queue. Will not queue more messages if this threshold is hit
107
+ attr_reader :max_queue_bytesize
108
+
109
+ ##
110
+ # The size of +logs_queue+ in bytes
111
+ attr_accessor :logs_queue_bytesize
112
+
113
+ ##
114
+ # The monotonic clock time when the last batch of logs were sent
115
+ attr_accessor :last_send_time
116
+
117
+ ##
118
+ # Number of seconds after the task completes before the task is performed again.
119
+ attr_reader :batch_interval
120
+
121
+ ##
122
+ # How many seconds to wait for data to be written while sending a batch
123
+ attr_reader :write_timeout
124
+
125
+ ##
126
+ # The maximum number of messages to post at one time
127
+ attr_reader :max_batch_size
128
+
129
+ ##
130
+ # The maximum number of bytes of a single message to include in a batch
131
+ attr_reader :max_message_bytesize
132
+
133
+ ##
134
+ # The maximum number of bytes that will be included in a single POST
135
+ attr_reader :max_batch_bytesize
136
+
137
+ ##
138
+ # The Concurrent executor class to use for asynchronous tasks
139
+ attr_reader :executor_class
140
+
141
+ ##
142
+ # The timer task thread
143
+ attr_reader :timer_task
144
+
145
+
146
+ ### LogDevice API -- write a message to the HTTP device.
147
+ def write( message )
148
+ self.start unless self.running?
149
+ if message.is_a?( Hash )
150
+ message_size = message.to_json.bytesize
151
+ else
152
+ message_size = message.bytesize
153
+ end
154
+ return if ( self.logs_queue_bytesize + message_size ) >= self.max_queue_bytesize
155
+ self.logs_queue_bytesize += message_size
156
+ self.logs_queue.enq( message )
157
+ self.send_logs
158
+ end
159
+
160
+
161
+ ### LogDevice API -- stop the batch thread and close the http connection
162
+ def close
163
+ self.stop
164
+ self.http_client.finish
165
+ rescue IOError
166
+ # ignore it since http session has not yet started.
167
+ end
168
+
169
+
170
+ ### Starts a thread pool with a single thread.
171
+ def start
172
+ self.start_executor
173
+ self.start_timer_task
174
+ end
175
+
176
+
177
+ ### Returns +true+ if the device has started sending messages to the logging endpoint.
178
+ def running?
179
+ return self.executor&.running?
180
+ end
181
+
182
+
183
+ ### Shutdown the executor, which is a pool of single thread
184
+ ### waits 3 seconds for shutdown to complete
185
+ def stop
186
+ return unless self.running?
187
+
188
+ self.timer_task.shutdown if self.timer_task&.running?
189
+ self.executor.shutdown
190
+
191
+ unless self.executor.wait_for_termination( 3 )
192
+ self.executor.halt
193
+ self.executor.wait_for_termination( 3 )
194
+ end
195
+ end
196
+
197
+
198
+ ### Start the background thread that sends messages.
199
+ def start_executor
200
+ @executor = self.executor_class.new
201
+ @executor.auto_terminate = true unless @executor.serialized?
202
+ end
203
+
204
+
205
+ ### Create a timer task that calls that sends logs at regular interval
206
+ def start_timer_task
207
+ @timer_task = Concurrent::TimerTask.execute( execution_interval: self.batch_interval ) do
208
+ self.send_logs
209
+ end
210
+ end
211
+
212
+
213
+ ### Sends a batch of log messages to the logging service. This executes inside
214
+ ### the sending thread.
215
+ def send_logs
216
+ self.executor.post do
217
+ if self.batch_ready?
218
+ # p "Batch ready; sending."
219
+ request = self.make_batch_request
220
+ request.body = self.get_next_log_payload
221
+
222
+ # p "Sending request", request
223
+
224
+ self.http_client.request( request ) do |res|
225
+ p( res ) if $DEBUG
226
+ end
227
+
228
+ self.last_send_time = Concurrent.monotonic_time
229
+ else
230
+ # p "Batch not ready yet."
231
+ end
232
+ end
233
+ end
234
+
235
+
236
+ ### Returns +true+ if a batch of logs is ready to be sent.
237
+ def batch_ready?
238
+ seconds_since_last_send = Concurrent.monotonic_time - self.last_send_time
239
+
240
+ return self.logs_queue.size >= self.max_batch_size ||
241
+ seconds_since_last_send >= self.batch_interval
242
+ end
243
+ alias_method :has_batch_ready?, :batch_ready?
244
+
245
+
246
+ ### Returns a new HTTP request (a subclass of Net::HTTPRequest) suitable for
247
+ ### sending the next batch of logs to the service. Defaults to a POST of JSON data. This
248
+ ### executes inside the sending thread.
249
+ def make_batch_request
250
+ request = Net::HTTP::Post.new( self.endpoint.path )
251
+ request[ 'Content-Type' ] = 'application/json'
252
+
253
+ return request
254
+ end
255
+
256
+
257
+ ### Dequeue pending log messages to send to the service and return them as a
258
+ ### suitably-encoded String. The default is a JSON Array. This executes inside
259
+ ### the sending thread.
260
+ def get_next_log_payload
261
+ buf = []
262
+ count = 0
263
+ bytes = 0
264
+
265
+ # Be conservative so as not to overflow
266
+ max_size = self.max_batch_bytesize - self.max_message_bytesize - 2 # for the outer Array
267
+
268
+ while count < self.max_batch_size && bytes < max_size && !self.logs_queue.empty?
269
+ message = self.logs_queue.deq
270
+ formatted_message = self.format_log_message( message )
271
+ self.logs_queue_bytesize -= message.bytesize
272
+
273
+ count += 1
274
+ bytes += formatted_message.bytesize + 3 # comma and delimiters
275
+
276
+ buf << formatted_message
277
+ end
278
+
279
+ return '[' + buf.join(',') + ']'
280
+ end
281
+
282
+
283
+ ### Returns the given +message+ in whatever format individual log messages are
284
+ ### expected to be in by the service. The default just returns the stringified
285
+ ### +message+. This executes inside the sending thread.
286
+ def format_log_message( message )
287
+ return message.to_s[ 0 ... self.max_message_bytesize ].dump
288
+ end
289
+
290
+
291
+ ### sets up a configured http object ready to instantiate connections
292
+ def http_client
293
+ return @http_client ||= begin
294
+ uri = URI( self.endpoint )
295
+
296
+ http = Net::HTTP.new( uri.host, uri.port )
297
+ http.write_timeout = self.write_timeout
298
+
299
+ if uri.scheme == 'https'
300
+ http.use_ssl = true
301
+ http.verify_mode = OpenSSL::SSL::VERIFY_PEER
302
+ end
303
+
304
+ http
305
+ end
306
+ end
307
+
308
+
309
+ end # class Loggability::LogDevice::Http
310
+