semantic_logger 3.4.1 → 4.0.0.beta1

Sign up to get free protection for your applications and to get access to all the features.
Files changed (48) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +10 -0
  3. data/Rakefile +4 -8
  4. data/lib/semantic_logger.rb +2 -31
  5. data/lib/semantic_logger/appender.rb +76 -0
  6. data/lib/semantic_logger/appender/bugsnag.rb +3 -8
  7. data/lib/semantic_logger/appender/file.rb +1 -1
  8. data/lib/semantic_logger/appender/honeybadger.rb +1 -1
  9. data/lib/semantic_logger/appender/http.rb +1 -1
  10. data/lib/semantic_logger/appender/mongodb.rb +30 -28
  11. data/lib/semantic_logger/appender/sentry.rb +2 -2
  12. data/lib/semantic_logger/appender/splunk_http.rb +4 -4
  13. data/lib/semantic_logger/appender/syslog.rb +2 -2
  14. data/lib/semantic_logger/appender/tcp.rb +9 -5
  15. data/lib/semantic_logger/appender/udp.rb +1 -0
  16. data/lib/semantic_logger/base.rb +73 -140
  17. data/lib/semantic_logger/core_ext/thread.rb +4 -1
  18. data/lib/semantic_logger/formatters/color.rb +7 -0
  19. data/lib/semantic_logger/formatters/default.rb +7 -0
  20. data/lib/semantic_logger/formatters/syslog.rb +1 -1
  21. data/lib/semantic_logger/log.rb +115 -12
  22. data/lib/semantic_logger/logger.rb +6 -215
  23. data/lib/semantic_logger/metrics/new_relic.rb +1 -1
  24. data/lib/semantic_logger/metrics/statsd.rb +5 -1
  25. data/lib/semantic_logger/metrics/udp.rb +80 -0
  26. data/lib/semantic_logger/processor.rb +235 -0
  27. data/lib/semantic_logger/semantic_logger.rb +36 -65
  28. data/lib/semantic_logger/subscriber.rb +2 -2
  29. data/lib/semantic_logger/version.rb +1 -1
  30. data/test/appender/bugsnag_test.rb +10 -9
  31. data/test/appender/elasticsearch_test.rb +3 -2
  32. data/test/appender/graylog_test.rb +4 -3
  33. data/test/appender/honeybadger_test.rb +2 -2
  34. data/test/appender/http_test.rb +3 -2
  35. data/test/appender/mongodb_test.rb +24 -23
  36. data/test/appender/new_relic_test.rb +15 -8
  37. data/test/appender/sentry_test.rb +2 -2
  38. data/test/appender/splunk_http_test.rb +8 -7
  39. data/test/appender/splunk_test.rb +6 -5
  40. data/test/appender/tcp_test.rb +3 -4
  41. data/test/appender/udp_test.rb +4 -5
  42. data/test/appender/wrapper_test.rb +37 -38
  43. data/test/concerns/compatibility_test.rb +2 -2
  44. data/test/loggable_test.rb +1 -1
  45. data/test/logger_test.rb +149 -528
  46. data/test/measure_test.rb +249 -0
  47. data/test/semantic_logger_test.rb +257 -0
  48. metadata +24 -16
@@ -1,6 +1,6 @@
1
1
  module SemanticLogger
2
2
  module Metrics
3
- class NewRelic
3
+ class NewRelic < Subscriber
4
4
  attr_accessor :prefix
5
5
 
6
6
  # Parameters:
@@ -7,7 +7,7 @@ end
7
7
 
8
8
  module SemanticLogger
9
9
  module Metrics
10
- class Statsd
10
+ class Statsd < Subscriber
11
11
  # Create Statsd metrics subscriber
12
12
  #
13
13
  # Parameters:
@@ -18,6 +18,10 @@ module SemanticLogger
18
18
  # Example, send all metrics to a particular namespace:
19
19
  # udp://localhost:8125/namespace
20
20
  # Default: udp://localhost:8125
21
+ #
22
+ # Example:
23
+ # subscriber = SemanticLogger::Metrics::Statsd.new(url: 'udp://localhost:8125')
24
+ # SemanticLogger.on_metric(subscriber)
21
25
  def initialize(options = {})
22
26
  options = options.dup
23
27
  @url = options.delete(:url) || 'udp://localhost:8125'
@@ -0,0 +1,80 @@
1
+ require 'socket'
2
+ module SemanticLogger
3
+ module Metrics
4
+ class Udp < Subscriber
5
+ attr_accessor :server, :separator, :udp_flags
6
+ attr_reader :socket
7
+
8
+ # Write metrics to in JSON format to Udp
9
+ #
10
+ # Parameters:
11
+ # server: [String]
12
+ # Host name and port to write UDP messages to
13
+ # Example:
14
+ # localhost:8125
15
+ #
16
+ # udp_flags: [Integer]
17
+ # Should be a bitwise OR of Socket::MSG_* constants.
18
+ # Default: 0
19
+ #
20
+ # Limitations:
21
+ # * UDP packet size is limited by the connected network and any routers etc
22
+ # that the message has to traverse. See https://en.wikipedia.org/wiki/Maximum_transmission_unit
23
+ #
24
+ # Example:
25
+ # subscriber = SemanticLogger::Metrics::Udp.new(server: 'localhost:8125')
26
+ # SemanticLogger.on_metric(subscriber)
27
+ def initialize(options = {}, &block)
28
+ options = options.dup
29
+ @server = options.delete(:server)
30
+ @udp_flags = options.delete(:udp_flags) || 0
31
+ raise(ArgumentError, 'Missing mandatory argument: :server') unless @server
32
+
33
+ super(options, &block)
34
+ reopen
35
+ end
36
+
37
+ # After forking an active process call #reopen to re-open
38
+ # open the handles to resources
39
+ def reopen
40
+ close
41
+ @socket = UDPSocket.new
42
+ host, port = server.split(':')
43
+ @socket.connect(host, port)
44
+ end
45
+
46
+ def call(log)
47
+ metric = log.metric
48
+ if duration = log.duration
49
+ @statsd.timing(metric, duration)
50
+ else
51
+ amount = (log.metric_amount || 1).round
52
+ if amount < 0
53
+ amount.times { @statsd.decrement(metric) }
54
+ else
55
+ amount.times { @statsd.increment(metric) }
56
+ end
57
+ end
58
+ @socket.send(data, udp_flags)
59
+ end
60
+
61
+ # Flush is called by the semantic_logger during shutdown.
62
+ def flush
63
+ @socket.flush if @socket
64
+ end
65
+
66
+ # Close is called during shutdown, or with reopen
67
+ def close
68
+ @socket.close if @socket
69
+ end
70
+
71
+ private
72
+
73
+ # Returns [SemanticLogger::Formatters::Default] formatter default for this Appender
74
+ def default_formatter
75
+ SemanticLogger::Formatters::Json.new
76
+ end
77
+
78
+ end
79
+ end
80
+ end
@@ -0,0 +1,235 @@
1
+ module SemanticLogger
2
+ # Thread that submits and processes log requests
3
+ class Processor
4
+ # Start the appender thread
5
+ def self.start
6
+ return false if active?
7
+ @@thread = Thread.new { process_requests }
8
+ raise 'Failed to start Appender Thread' unless @@thread
9
+ true
10
+ end
11
+
12
+ # Returns true if the appender_thread is active
13
+ def self.active?
14
+ @@thread && @@thread.alive?
15
+ end
16
+
17
+ # Returns [Integer] the number of log entries waiting to be written to the appenders
18
+ def self.queue_size
19
+ queue.size
20
+ end
21
+
22
+ # Add log request to the queue for processing
23
+ def self.<<(log)
24
+ queue << log if active?
25
+ end
26
+
27
+ # Submit command and wait for reply
28
+ def self.submit_request(command)
29
+ return false unless active?
30
+
31
+ msg = "Too many queued log messages: #{queue_size}, running command: #{command}"
32
+ if queue_size > 1_000
33
+ logger.warn msg
34
+ elsif queue_size > 100
35
+ logger.info msg
36
+ elsif queue_size > 0
37
+ logger.trace msg
38
+ end
39
+
40
+ reply_queue = Queue.new
41
+ queue << {command: command, reply_queue: reply_queue}
42
+ reply_queue.pop
43
+ end
44
+
45
+ # Allow the internal logger to be overridden from its default of STDERR
46
+ # Can be replaced with another Ruby logger or Rails logger, but never to
47
+ # SemanticLogger::Logger itself since it is for reporting problems
48
+ # while trying to log to the various appenders
49
+ def self.logger=(logger)
50
+ @@logger = logger
51
+ end
52
+
53
+ # Returns the check_interval which is the number of messages between checks
54
+ # to determine if the appender thread is falling behind
55
+ def self.lag_check_interval
56
+ @@lag_check_interval
57
+ end
58
+
59
+ # Set the check_interval which is the number of messages between checks
60
+ # to determine if the appender thread is falling behind
61
+ def self.lag_check_interval=(lag_check_interval)
62
+ @@lag_check_interval = lag_check_interval
63
+ end
64
+
65
+ # Returns the amount of time in seconds
66
+ # to determine if the appender thread is falling behind
67
+ def self.lag_threshold_s
68
+ @@lag_threshold_s
69
+ end
70
+
71
+ # Supply a metrics appender to be called whenever a logging metric is encountered
72
+ #
73
+ # Parameters
74
+ # appender: [Symbol | Object | Proc]
75
+ # [Proc] the block to call.
76
+ # [Object] the block on which to call #call.
77
+ # [Symbol] :new_relic, or :statsd to forward metrics to
78
+ #
79
+ # block
80
+ # The block to be called
81
+ #
82
+ # Example:
83
+ # SemanticLogger.on_metric do |log|
84
+ # puts "#{log.metric} was received. Log: #{log.inspect}"
85
+ # end
86
+ #
87
+ # Note:
88
+ # * This callback is called in the logging thread.
89
+ # * Does not slow down the application.
90
+ # * Only context is what is passed in the log struct, the original thread context is not available.
91
+ def self.on_metric(options = {}, &block)
92
+ # Backward compatibility
93
+ options = options.is_a?(Hash) ? options.dup : {appender: options}
94
+ appender = block || options.delete(:appender)
95
+
96
+ # Convert symbolized metrics appender to an actual object
97
+ appender = SemanticLogger::Appender.constantize_symbol(appender, 'SemanticLogger::Metrics').new(options) if appender.is_a?(Symbol)
98
+
99
+ raise('When supplying a metrics appender, it must support the #call method') unless appender.is_a?(Proc) || appender.respond_to?(:call)
100
+ (@@metric_subscribers ||= Concurrent::Array.new) << appender
101
+ end
102
+
103
+ private
104
+
105
+ @@thread = nil
106
+ @@queue = Queue.new
107
+ @@lag_check_interval = 5000
108
+ @@lag_threshold_s = 30
109
+ @@metric_subscribers = nil
110
+
111
+ # Queue to hold messages that need to be logged to the various appenders
112
+ def self.queue
113
+ @@queue
114
+ end
115
+
116
+ # Internal logger for SemanticLogger
117
+ # For example when an appender is not working etc..
118
+ # By default logs to STDERR
119
+ def self.logger
120
+ @@logger ||= begin
121
+ l = SemanticLogger::Appender::File.new(STDERR, :warn)
122
+ l.name = name
123
+ l
124
+ end
125
+ end
126
+
127
+ # Separate appender thread responsible for reading log messages and
128
+ # calling the appenders in it's thread
129
+ def self.process_requests
130
+ # This thread is designed to never go down unless the main thread terminates
131
+ # Before terminating at_exit is used to flush all the appenders
132
+ #
133
+ # Should any appender fail to log or flush, the exception is logged and
134
+ # other appenders will still be called
135
+ Thread.current.name = 'SemanticLogger::Processor'
136
+ logger.trace "V#{VERSION} Processor thread active"
137
+ begin
138
+ count = 0
139
+ while message = queue.pop
140
+ if message.is_a?(Log)
141
+ call_appenders(message)
142
+ call_metric_subscribers(message) if message.metric
143
+ count += 1
144
+ # Check every few log messages whether this appender thread is falling behind
145
+ if count > lag_check_interval
146
+ if (diff = Time.now - message.time) > lag_threshold_s
147
+ logger.warn "Appender thread has fallen behind by #{diff} seconds with #{queue_size} messages queued up. Consider reducing the log level or changing the appenders"
148
+ end
149
+ count = 0
150
+ end
151
+ else
152
+ case message[:command]
153
+ when :flush
154
+ flush_appenders
155
+ message[:reply_queue] << true if message[:reply_queue]
156
+ when :close
157
+ close_appenders
158
+ message[:reply_queue] << true if message[:reply_queue]
159
+ else
160
+ logger.warn "Appender thread: Ignoring unknown command: #{message[:command]}"
161
+ end
162
+ end
163
+ end
164
+ rescue Exception => exception
165
+ # This block may be called after the file handles have been released by Ruby
166
+ begin
167
+ logger.error 'Appender thread restarting due to exception', exception
168
+ rescue Exception
169
+ nil
170
+ end
171
+ retry
172
+ ensure
173
+ @@thread = nil
174
+ # This block may be called after the file handles have been released by Ruby
175
+ begin
176
+ logger.trace 'Appender thread has stopped'
177
+ rescue Exception
178
+ nil
179
+ end
180
+ end
181
+ end
182
+
183
+ # Call Metric subscribers
184
+ def self.call_metric_subscribers(log)
185
+ # If no subscribers registered, then return immediately
186
+ return unless @@metric_subscribers
187
+
188
+ @@metric_subscribers.each do |subscriber|
189
+ begin
190
+ subscriber.call(log)
191
+ rescue Exception => exc
192
+ logger.error 'Exception calling metrics subscriber', exc
193
+ end
194
+ end
195
+ end
196
+
197
+ # Call Appenders
198
+ def self.call_appenders(log)
199
+ SemanticLogger.appenders.each do |appender|
200
+ begin
201
+ appender.log(log)
202
+ rescue Exception => exc
203
+ logger.error "Appender thread: Failed to log to appender: #{appender.inspect}", exc
204
+ end
205
+ end
206
+ end
207
+
208
+ def self.flush_appenders
209
+ SemanticLogger.appenders.each do |appender|
210
+ begin
211
+ logger.trace "Appender thread: Flushing appender: #{appender.name}"
212
+ appender.flush
213
+ rescue Exception => exc
214
+ logger.error "Appender thread: Failed to flush appender: #{appender.inspect}", exc
215
+ end
216
+ end
217
+ logger.trace 'Appender thread: All appenders flushed'
218
+ end
219
+
220
+ def self.close_appenders
221
+ SemanticLogger.appenders.each do |appender|
222
+ begin
223
+ logger.trace "Appender thread: Closing appender: #{appender.name}"
224
+ appender.flush
225
+ appender.close
226
+ SemanticLogger.remove_appender(appender)
227
+ rescue Exception => exc
228
+ logger.error "Appender thread: Failed to close appender: #{appender.inspect}", exc
229
+ end
230
+ end
231
+ logger.trace 'Appender thread: All appenders closed and removed from appender list'
232
+ end
233
+
234
+ end
235
+ end
@@ -153,11 +153,11 @@ module SemanticLogger
153
153
  # logger.debug("Login time", user: 'Joe', duration: 100, ip_address: '127.0.0.1')
154
154
  def self.add_appender(options, deprecated_level = nil, &block)
155
155
  options = options.is_a?(Hash) ? options.dup : convert_old_appender_args(options, deprecated_level)
156
- appender = appender_from_options(options, &block)
156
+ appender = SemanticLogger::Appender.create(options, &block)
157
157
  @@appenders << appender
158
158
 
159
159
  # Start appender thread if it is not already running
160
- SemanticLogger::Logger.start_appender_thread
160
+ SemanticLogger::Processor.start
161
161
  appender
162
162
  end
163
163
 
@@ -193,7 +193,7 @@ module SemanticLogger
193
193
  def self.reopen
194
194
  @@appenders.each { |appender| appender.reopen if appender.respond_to?(:reopen) }
195
195
  # After a fork the appender thread is not running, start it if it is not running
196
- SemanticLogger::Logger.start_appender_thread
196
+ SemanticLogger::Processor.start
197
197
  end
198
198
 
199
199
  # Supply a block to be called whenever a metric is seen during measure logging
@@ -209,7 +209,7 @@ module SemanticLogger
209
209
  #
210
210
  # Example:
211
211
  # SemanticLogger.on_metric do |log|
212
- # puts "#{log.metric} was received. Log Struct: #{log.inspect}"
212
+ # puts "#{log.metric} was received. Log: #{log.inspect}"
213
213
  # end
214
214
  #
215
215
  # Note:
@@ -217,7 +217,7 @@ module SemanticLogger
217
217
  # * Does not slow down the application.
218
218
  # * Only context is what is passed in the log struct, the original thread context is not available.
219
219
  def self.on_metric(options = {}, &block)
220
- SemanticLogger::Logger.on_metric(options, &block)
220
+ SemanticLogger::Processor.on_metric(options, &block)
221
221
  end
222
222
 
223
223
  # Add signal handlers for Semantic Logger
@@ -267,14 +267,7 @@ module SemanticLogger
267
267
  logger = SemanticLogger['Thread Dump']
268
268
  Thread.list.each do |thread|
269
269
  next if thread == Thread.current
270
- message = thread.name
271
- if backtrace = thread.backtrace
272
- message += "\n"
273
- message << backtrace.join("\n")
274
- end
275
- tags = thread[:semantic_logger_tags]
276
- tags = tags.nil? ? [] : tags.clone
277
- logger.tagged(tags) { logger.warn(message) }
270
+ logger.backtrace(thread: thread)
278
271
  end
279
272
  end if thread_dump_signal
280
273
 
@@ -297,19 +290,6 @@ module SemanticLogger
297
290
  Thread.current[:semantic_logger_tags].pop
298
291
  end
299
292
 
300
- # Add the supplied named tags to the list of tags to log for this thread whilst
301
- # the supplied block is active.
302
- #
303
- # Returns result of block
304
- #
305
- # Example:
306
- def self.named_tags(tag)
307
- (Thread.current[:semantic_logger_tags] ||= []) << tag
308
- yield
309
- ensure
310
- Thread.current[:semantic_logger_tags].pop
311
- end
312
-
313
293
  # Add the supplied tags to the list of tags to log for this thread whilst
314
294
  # the supplied block is active.
315
295
  # Returns result of block
@@ -344,6 +324,36 @@ module SemanticLogger
344
324
  t.pop(quantity) unless t.nil?
345
325
  end
346
326
 
327
+ # Add the supplied named tags to the list of tags to log for this thread whilst
328
+ # the supplied block is active.
329
+ #
330
+ # Add a payload to all log calls on This Thread within the supplied block
331
+ #
332
+ # SemanticLogger.named_tagged(tracking_number: 12345) do
333
+ # logger.debug('Hello World')
334
+ # end
335
+ #
336
+ # Returns result of block
337
+ def self.named_tagged(hash)
338
+ raise(ArgumentError, '#named_tagged only accepts named parameters (Hash)') unless hash.is_a?(Hash)
339
+ (Thread.current[:semantic_logger_named_tags] ||= []) << hash
340
+ yield
341
+ ensure
342
+ Thread.current[:semantic_logger_named_tags].pop
343
+ end
344
+
345
+ # Returns a copy of the [Hash] of named tags currently active for this thread
346
+ # Returns nil if no named tags are set
347
+ def self.named_tags
348
+ if (list = Thread.current[:semantic_logger_named_tags]) && !list.empty?
349
+ if list.size > 1
350
+ list.inject({}) { |h, sum| sum.merge(h) }
351
+ else
352
+ list.first.clone
353
+ end
354
+ end
355
+ end
356
+
347
357
  # Silence noisy log levels by changing the default_level within the block
348
358
  #
349
359
  # This setting is thread-safe and only applies to the current thread
@@ -445,45 +455,6 @@ module SemanticLogger
445
455
  options
446
456
  end
447
457
 
448
- # Returns [SemanticLogger::Subscriber] appender for the supplied options
449
- def self.appender_from_options(options, &block)
450
- if options[:io] || options[:file_name]
451
- SemanticLogger::Appender::File.new(options, &block)
452
- elsif appender = options.delete(:appender)
453
- if appender.is_a?(Symbol)
454
- constantize_symbol(appender).new(options)
455
- elsif appender.is_a?(Subscriber)
456
- appender
457
- else
458
- raise(ArgumentError, "Parameter :appender must be either a Symbol or an object derived from SemanticLogger::Subscriber, not: #{appender.inspect}")
459
- end
460
- elsif options[:logger]
461
- SemanticLogger::Appender::Wrapper.new(options, &block)
462
- end
463
- end
464
-
465
- def self.constantize_symbol(symbol, namespace = 'SemanticLogger::Appender')
466
- klass = "#{namespace}::#{camelize(symbol.to_s)}"
467
- begin
468
- if RUBY_VERSION.to_i >= 2
469
- Object.const_get(klass)
470
- else
471
- klass.split('::').inject(Object) { |o, name| o.const_get(name) }
472
- end
473
- rescue NameError
474
- raise(ArgumentError, "Could not convert symbol: #{symbol} to a class in: #{namespace}. Looking for: #{klass}")
475
- end
476
- end
477
-
478
- # Borrow from Rails, when not running Rails
479
- def self.camelize(term)
480
- string = term.to_s
481
- string = string.sub(/^[a-z\d]*/) { |match| match.capitalize }
482
- string.gsub!(/(?:_|(\/))([a-z\d]*)/i) { "#{$1}#{$2.capitalize}" }
483
- string.gsub!('/'.freeze, '::'.freeze)
484
- string
485
- end
486
-
487
458
  # Initial default Level for all new instances of SemanticLogger::Logger
488
459
  @@default_level = :info
489
460
  @@default_level_index = level_to_index(@@default_level)