semantic_logger 3.4.1 → 4.0.0.beta1

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 (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)