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.
- checksums.yaml +5 -5
- checksums.yaml.gz.sig +0 -0
- data.tar.gz.sig +0 -0
- data/History.rdoc +55 -0
- data/Manifest.txt +15 -4
- data/{README.rdoc → README.md} +78 -71
- data/Rakefile +5 -77
- data/lib/loggability.rb +19 -27
- data/lib/loggability/constants.rb +2 -2
- data/lib/loggability/formatter.rb +13 -67
- data/lib/loggability/formatter/color.rb +2 -2
- data/lib/loggability/formatter/default.rb +69 -6
- data/lib/loggability/formatter/html.rb +5 -5
- data/lib/loggability/formatter/structured.rb +35 -0
- data/lib/loggability/log_device.rb +86 -0
- data/lib/loggability/log_device/appending.rb +34 -0
- data/lib/loggability/log_device/datadog.rb +90 -0
- data/lib/loggability/log_device/file.rb +37 -0
- data/lib/loggability/log_device/http.rb +310 -0
- data/lib/loggability/logclient.rb +2 -1
- data/lib/loggability/logger.rb +45 -42
- data/lib/loggability/loghost.rb +2 -1
- data/lib/loggability/override.rb +2 -1
- data/lib/loggability/spechelpers.rb +1 -1
- data/spec/helpers.rb +6 -12
- data/spec/loggability/formatter/color_spec.rb +3 -7
- data/spec/loggability/formatter/default_spec.rb +50 -0
- data/spec/loggability/formatter/html_spec.rb +7 -7
- data/spec/loggability/formatter/structured_spec.rb +61 -0
- data/spec/loggability/formatter_spec.rb +42 -29
- data/spec/loggability/log_device/appending_spec.rb +27 -0
- data/spec/loggability/log_device/datadog_spec.rb +67 -0
- data/spec/loggability/log_device/file_spec.rb +27 -0
- data/spec/loggability/log_device/http_spec.rb +217 -0
- data/spec/loggability/logger_spec.rb +46 -5
- data/spec/loggability/loghost_spec.rb +3 -1
- data/spec/loggability/override_spec.rb +18 -5
- data/spec/loggability/spechelpers_spec.rb +3 -4
- data/spec/loggability_spec.rb +16 -13
- metadata +77 -105
- metadata.gz.sig +0 -0
- 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
|
+
|