semantic_logger 4.1.1 → 4.2.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 (70) hide show
  1. checksums.yaml +4 -4
  2. data/lib/semantic_logger.rb +6 -13
  3. data/lib/semantic_logger/ansi_colors.rb +10 -10
  4. data/lib/semantic_logger/appender.rb +42 -26
  5. data/lib/semantic_logger/appender/async.rb +179 -0
  6. data/lib/semantic_logger/appender/async_batch.rb +95 -0
  7. data/lib/semantic_logger/appender/bugsnag.rb +2 -1
  8. data/lib/semantic_logger/appender/elasticsearch.rb +113 -81
  9. data/lib/semantic_logger/appender/elasticsearch_http.rb +1 -3
  10. data/lib/semantic_logger/appender/file.rb +1 -3
  11. data/lib/semantic_logger/appender/graylog.rb +6 -5
  12. data/lib/semantic_logger/appender/honeybadger.rb +0 -2
  13. data/lib/semantic_logger/appender/http.rb +25 -10
  14. data/lib/semantic_logger/appender/kafka.rb +1 -3
  15. data/lib/semantic_logger/appender/mongodb.rb +1 -3
  16. data/lib/semantic_logger/appender/new_relic.rb +7 -3
  17. data/lib/semantic_logger/appender/sentry.rb +6 -7
  18. data/lib/semantic_logger/appender/splunk.rb +1 -2
  19. data/lib/semantic_logger/appender/splunk_http.rb +3 -4
  20. data/lib/semantic_logger/appender/syslog.rb +1 -3
  21. data/lib/semantic_logger/appender/tcp.rb +7 -9
  22. data/lib/semantic_logger/appender/udp.rb +0 -2
  23. data/lib/semantic_logger/appender/wrapper.rb +0 -2
  24. data/lib/semantic_logger/base.rb +76 -19
  25. data/lib/semantic_logger/formatters.rb +37 -0
  26. data/lib/semantic_logger/formatters/base.rb +10 -3
  27. data/lib/semantic_logger/formatters/json.rb +2 -6
  28. data/lib/semantic_logger/formatters/one_line.rb +18 -0
  29. data/lib/semantic_logger/formatters/raw.rb +8 -2
  30. data/lib/semantic_logger/formatters/signalfx.rb +169 -0
  31. data/lib/semantic_logger/log.rb +23 -14
  32. data/lib/semantic_logger/loggable.rb +88 -15
  33. data/lib/semantic_logger/logger.rb +0 -20
  34. data/lib/semantic_logger/metric/new_relic.rb +75 -0
  35. data/lib/semantic_logger/metric/signalfx.rb +123 -0
  36. data/lib/semantic_logger/{metrics → metric}/statsd.rb +20 -8
  37. data/lib/semantic_logger/processor.rb +67 -169
  38. data/lib/semantic_logger/semantic_logger.rb +7 -31
  39. data/lib/semantic_logger/subscriber.rb +32 -36
  40. data/lib/semantic_logger/utils.rb +47 -0
  41. data/lib/semantic_logger/version.rb +1 -1
  42. data/test/appender/async_batch_test.rb +61 -0
  43. data/test/appender/async_test.rb +45 -0
  44. data/test/appender/elasticsearch_http_test.rb +3 -3
  45. data/test/appender/elasticsearch_test.rb +211 -49
  46. data/test/appender/file_test.rb +9 -8
  47. data/test/appender/mongodb_test.rb +3 -3
  48. data/test/appender/newrelic_rpm.rb +6 -0
  49. data/test/appender/sentry_test.rb +3 -1
  50. data/test/appender/wrapper_test.rb +29 -0
  51. data/test/concerns/compatibility_test.rb +64 -60
  52. data/test/debug_as_trace_logger_test.rb +62 -77
  53. data/test/formatters/one_line_test.rb +61 -0
  54. data/test/formatters/signalfx_test.rb +200 -0
  55. data/test/formatters_test.rb +36 -0
  56. data/test/in_memory_appender.rb +9 -0
  57. data/test/in_memory_appender_helper.rb +43 -0
  58. data/test/in_memory_batch_appender.rb +9 -0
  59. data/test/in_memory_metrics_appender.rb +14 -0
  60. data/test/loggable_test.rb +15 -30
  61. data/test/logger_test.rb +181 -135
  62. data/test/measure_test.rb +212 -113
  63. data/test/metric/new_relic_test.rb +36 -0
  64. data/test/metric/signalfx_test.rb +78 -0
  65. data/test/semantic_logger_test.rb +58 -65
  66. data/test/test_helper.rb +19 -2
  67. metadata +33 -7
  68. data/lib/semantic_logger/metrics/new_relic.rb +0 -30
  69. data/lib/semantic_logger/metrics/udp.rb +0 -80
  70. data/test/mock_logger.rb +0 -29
@@ -47,7 +47,10 @@ module SemanticLogger
47
47
  # context [Hash]
48
48
  # Named contexts that were captured when the log entry was created.
49
49
  class Log
50
- attr_accessor :level, :thread_name, :name, :message, :payload, :time, :duration, :tags, :level_index, :exception, :metric, :backtrace, :metric_amount, :named_tags, :context
50
+ attr_accessor :level, :level_index, :name, :message, :time, :duration,
51
+ :payload, :exception, :thread_name, :backtrace,
52
+ :tags, :named_tags, :context,
53
+ :metric, :metric_amount, :dimensions
51
54
 
52
55
  def initialize(name, level, index = nil)
53
56
  @level = level
@@ -65,7 +68,17 @@ module SemanticLogger
65
68
  #
66
69
  # Example:
67
70
  # logger.info(name: 'value')
68
- def assign(message: nil, payload: nil, min_duration: 0.0, exception: nil, metric: nil, metric_amount: 1, duration: nil, backtrace: nil, log_exception: :full, on_exception_level: nil)
71
+ def assign(message: nil,
72
+ payload: nil,
73
+ min_duration: 0.0,
74
+ exception: nil,
75
+ metric: nil,
76
+ metric_amount: nil,
77
+ duration: nil,
78
+ backtrace: nil,
79
+ log_exception: :full,
80
+ on_exception_level: nil,
81
+ dimensions: nil)
69
82
  # Elastic logging: Log when :duration exceeds :min_duration
70
83
  # Except if there is an exception when it will always be logged
71
84
  if duration
@@ -95,14 +108,15 @@ module SemanticLogger
95
108
  end
96
109
 
97
110
  if backtrace
98
- self.backtrace = self.class.cleanse_backtrace(backtrace)
111
+ self.backtrace = Utils.cleanse_backtrace(backtrace)
99
112
  elsif level_index >= SemanticLogger.backtrace_level_index
100
- self.backtrace = self.class.cleanse_backtrace
113
+ self.backtrace = Utils.cleanse_backtrace
101
114
  end
102
115
 
103
116
  if metric
104
117
  self.metric = metric
105
118
  self.metric_amount = metric_amount
119
+ self.dimensions = dimensions
106
120
  end
107
121
 
108
122
  self.payload = payload if payload && (payload.size > 0)
@@ -125,6 +139,8 @@ module SemanticLogger
125
139
  elsif exception.nil? && payload && payload.respond_to?(:backtrace) && payload.respond_to?(:message)
126
140
  exception = payload
127
141
  payload = nil
142
+ elsif payload.is_a?(String)
143
+ message = message.nil? ? payload : "#{message} -- #{payload}"
128
144
  end
129
145
 
130
146
  # Add result of block as message or payload if not nil
@@ -277,16 +293,9 @@ module SemanticLogger
277
293
  (self.context ||= {})[key] = value
278
294
  end
279
295
 
280
- private
281
-
282
- SELF_PATTERN = File.join('lib', 'semantic_logger')
283
-
284
- # Extract the backtrace leaving out Semantic Logger
285
- def self.cleanse_backtrace(stack = caller)
286
- while (first = stack.first) && first.include?(SELF_PATTERN)
287
- stack.shift
288
- end
289
- stack
296
+ # A metric only event has a metric but no message, exception, or payload.
297
+ def metric_only?
298
+ metric && message.nil? && exception.nil? && payload.nil?
290
299
  end
291
300
 
292
301
  end
@@ -5,29 +5,34 @@
5
5
  # By including this mix-in into any class it will define a class level logger
6
6
  # and also make it accessible via instance methods
7
7
  #
8
- # Example
8
+ # Example:
9
+ # require 'semantic_logger'
10
+ # SemanticLogger.default_level = :debug
11
+ # SemanticLogger.add_appender(io: STDOUT, formatter: :color)
9
12
  #
10
- # require 'semantic_logger'
11
- # SemanticLogger.default_level = :debug
12
- # SemanticLogger.add_appender(io: STDOUT, formatter: :color)
13
+ # class ExternalSupplier
14
+ # # Create class and instance logger methods
15
+ # include SemanticLogger::Loggable
13
16
  #
14
- # class ExternalSupplier
15
- # # Create class and instance logger methods
16
- # include SemanticLogger::Loggable
17
+ # def call_supplier(amount, name)
18
+ # logger.debug "Calculating with amount", { amount: amount, name: name }
17
19
  #
18
- # def call_supplier(amount, name)
19
- # logger.debug "Calculating with amount", { amount: amount, name: name }
20
+ # # Measure and log on completion how long the call took to the external supplier
21
+ # logger.measure_info "Calling external interface" do
22
+ # # Code to call the external supplier ...
23
+ # end
24
+ # end
25
+ # end
20
26
  #
21
- # # Measure and log on completion how long the call took to the external supplier
22
- # logger.measure_info "Calling external interface" do
23
- # # Code to call the external supplier ...
24
- # end
25
- # end
26
- # end
27
+ # Notes:
28
+ # * To forcibly replace Rails or any other existing logging methods
29
+ # use `prepend` instead of `include`. For example:
30
+ # ExternalSupplier.prepend SemanticLogger::Loggable
27
31
  module SemanticLogger
28
32
  module Loggable
29
33
 
30
34
  def self.included(base)
35
+ base.extend ClassMethods
31
36
  base.class_eval do
32
37
  # Returns [SemanticLogger::Logger] class level logger
33
38
  def self.logger
@@ -51,5 +56,73 @@ module SemanticLogger
51
56
  end
52
57
  end
53
58
 
59
+ module ClassMethods
60
+ # Measure and log the performance of an instance method.
61
+ #
62
+ # Parameters:
63
+ # method_name: [Symbol]
64
+ # The name of the method that should be measured.
65
+ #
66
+ # options: [Hash]
67
+ # Any valid options that can be passed to measure.
68
+ #
69
+ # Approximate overhead when logging a method call with a metric:
70
+ # 0.044 ms per method call.
71
+ # 0.009 ms per method call. If `min_duration` is not met
72
+ # 0.0005 ms per method call. If `level` is not met
73
+ def logger_measure_method(method_name,
74
+ min_duration: 0.0,
75
+ metric: "#{name}/#{method_name}",
76
+ log_exception: :partial,
77
+ on_exception_level: nil,
78
+ message: "##{method_name}",
79
+ level: :info)
80
+
81
+ # unless visibility = Utils.method_visibility(self, method_name)
82
+ # logger.warn("Unable to measure method: #{name}##{method_name} since it does not exist")
83
+ # return false
84
+ # end
85
+
86
+ index = SemanticLogger.level_to_index(level)
87
+
88
+ logger_measure_module.module_eval(<<-EOT, __FILE__, __LINE__ + 1)
89
+ def #{method_name}(*args, &block)
90
+ if logger.send(:level_index) <= #{index}
91
+ logger.send(
92
+ :measure_method,
93
+ index: #{index},
94
+ level: #{level.inspect},
95
+ message: #{message.inspect},
96
+ min_duration: #{min_duration},
97
+ metric: #{metric.inspect},
98
+ log_exception: #{log_exception.inspect},
99
+ on_exception_level: #{on_exception_level.inspect}
100
+ ) do
101
+ super(*args, &block)
102
+ end
103
+ else
104
+ super(*args, &block)
105
+ end
106
+ end
107
+ EOT
108
+ #{"#{visibility} :#{method_name}" unless visibility == :public}
109
+ true
110
+ end
111
+
112
+ private
113
+
114
+ # Dynamic Module to intercept method calls for measuring purposes.
115
+ def logger_measure_module
116
+ if const_defined?(:SemanticLoggerMeasure, _search_ancestors = false)
117
+ const_get(:SemanticLoggerMeasure)
118
+ else
119
+ mod = const_set(:SemanticLoggerMeasure, Module.new)
120
+ prepend mod
121
+ mod
122
+ end
123
+ end
124
+
125
+ end
126
+
54
127
  end
55
128
  end
@@ -38,25 +38,5 @@ module SemanticLogger
38
38
  Processor << log
39
39
  end
40
40
 
41
- # DEPRECATED
42
- def self.queue_size
43
- Processor.queue_size
44
- end
45
-
46
- # DEPRECATED
47
- def self.flush
48
- Processor.flush
49
- end
50
-
51
- # DEPRECATED
52
- def self.close
53
- Processor.close
54
- end
55
-
56
- # DEPRECATED
57
- def self.logger=(logger)
58
- Processor.logger = logger
59
- end
60
-
61
41
  end
62
42
  end
@@ -0,0 +1,75 @@
1
+ begin
2
+ require 'newrelic_rpm'
3
+ rescue LoadError
4
+ raise 'Gem newrelic_rpm is required for logging to New Relic. Please add the gem "newrelic_rpm" to your Gemfile.'
5
+ end
6
+
7
+ # Send Metrics to NewRelic
8
+ #
9
+ # The :error and :fatal log entries will show up under
10
+ # "Applications" > "Application Name" > "Events" > "Errors" in New Relic.
11
+ #
12
+ # Example:
13
+ # SemanticLogger.add_appender(metric: :new_relic)
14
+ class SemanticLogger::Metric::NewRelic < SemanticLogger::Subscriber
15
+ attr_accessor :prefix
16
+
17
+ # Create Appender
18
+ #
19
+ # Parameters
20
+ # :prefix [String]
21
+ # Prefix to add to every metric before forwarding to NewRelic.
22
+ # Default: 'Custom'
23
+ #
24
+ # level: [:trace | :debug | :info | :warn | :error | :fatal]
25
+ # Override the log level for this appender.
26
+ # Default: :error
27
+ #
28
+ # formatter: [Object|Proc]
29
+ # An instance of a class that implements #call, or a Proc to be used to format
30
+ # the output from this appender
31
+ # Default: Use the built-in formatter (See: #call)
32
+ #
33
+ # filter: [Regexp|Proc]
34
+ # RegExp: Only include log messages where the class name matches the supplied.
35
+ # regular expression. All other messages will be ignored.
36
+ # Proc: Only include log messages where the supplied Proc returns true
37
+ # The Proc must return true or false.
38
+ def initialize(prefix: 'Custom',
39
+ level: nil,
40
+ formatter: nil,
41
+ filter: nil,
42
+ application: nil,
43
+ host: nil,
44
+ &block)
45
+
46
+ @prefix = prefix
47
+ super(level: level, formatter: formatter, filter: filter, application: application, host: host, &block)
48
+ end
49
+
50
+ # Returns metric name to use.
51
+ def call(log, _logger)
52
+ metric = log.metric
53
+ # Add prefix for NewRelic
54
+ metric = "#{prefix}/#{metric}" unless metric.start_with?(prefix)
55
+ metric
56
+ end
57
+
58
+ def log(log)
59
+ name = formatter.call(log, self)
60
+ if duration = log.duration
61
+ # Convert duration to seconds
62
+ ::NewRelic::Agent.record_metric(name, duration / 1000.0)
63
+ else
64
+ ::NewRelic::Agent.increment_metric(name, log.metric_amount || 1)
65
+ end
66
+ true
67
+ end
68
+
69
+ # Only forward log entries that contain metrics.
70
+ def should_log?(log)
71
+ # Does not support metrics with dimensions.
72
+ log.metric && !log.dimensions && meets_log_level?(log) && !filtered?(log)
73
+ end
74
+
75
+ end
@@ -0,0 +1,123 @@
1
+ # Forward application metrics to SignalFx.
2
+ #
3
+ # Example:
4
+ # SemanticLogger.add_appender(
5
+ # metric: :signalfx,
6
+ # token: 'SIGNALFX_ORG_ACCESS_TOKEN'
7
+ # )
8
+ class SemanticLogger::Metric::Signalfx < SemanticLogger::Appender::Http
9
+ attr_reader :full_url
10
+
11
+ END_POINT = 'v2/datapoint'
12
+
13
+ # Create SignalFx metrics appender.
14
+ #
15
+ # Parameters:
16
+ # token: [String]
17
+ # Access Token to use for sending metrics.
18
+ # Obtain the Signalfx token via the Signalfx Web UI under `Organization` -> `Access Tokens`.
19
+ #
20
+ # dimensions: [Array<String>]
21
+ # Dimensions to forward to signalfx when they are present in the named tags of any log message.
22
+ # By default `application` and `host` are always included as dimensions in all forwarded metrics.
23
+ # Example: [:user_id, :state]
24
+ #
25
+ # filter: [Regexp|Proc]
26
+ # RegExp: Only include log messages where the class name matches the supplied
27
+ # regular expression. All other messages will be ignored.
28
+ # Proc: Only include log messages where the supplied Proc returns true.
29
+ # The Proc must return true or false.
30
+ #
31
+ # host: [String]
32
+ # Name of this host to send as a dimension.
33
+ # Default: SemanticLogger.host
34
+ #
35
+ # application: [String]
36
+ # Name of this application to send as a dimension.
37
+ # Default: SemanticLogger.application
38
+ #
39
+ # url: [String]
40
+ # Override the SignalFx service url.
41
+ # For historical data use: https://backfill.signalfx.com/v1/backfill
42
+ # Default: https://ingest.signalfx.com
43
+ #
44
+ # Notes:
45
+ #
46
+ # When sending a metric to Signalfx, it is necessary to send both a `gauge` and a `counter` when a
47
+ # duration is included in the metric, otherwise it is not possible to chart counts of the metric.
48
+ # Unfortunately this doubles the number of metrics, but it is the way Signalfx works.
49
+ # Using a `count` of a `gauge` in a chart will significantly under-count the number of occurrences.
50
+ #
51
+ # If dimensions are added to the metric, then the metric will be sent as-is and
52
+ # the above logic will _not_ be applied.
53
+ #
54
+ # Example, Gauge metric, supplying the duration in `metric_amount`:
55
+ # logger.info(metric: 'Filters.average', metric_amount: 1.2, dimensions: {user: 'jbloggs'})
56
+ #
57
+ # Example, Counter metric:
58
+ # logger.info(metric: 'Filters.count', dimensions: {user: 'jbloggs'})
59
+ #
60
+ # Example, Counter metric with a count other than 1:
61
+ # logger.info(metric: 'Filters.count', metric_amount: 23, dimensions: {user: 'jbloggs'})
62
+ #
63
+ # When a duration is supplied and no dimensions are supplied:
64
+ # logger.info(metric: 'Common/User/authorize', duration: 1.4)
65
+ #
66
+ # Then it is translated into the following 2 log entries under the covers:
67
+ # logger.info(metric: 'Application.average', metric_amount: 1.4, dimensions: {class: 'Common::User', action: 'authorize'})
68
+ # logger.info(metric: 'Application.counter', metric_amount: 1, dimensions: {class: 'Common::User', action: 'authorize'})
69
+ #
70
+ # Similarly with a measure block which automatically supplies the duration:
71
+ # logger.measure_info(metric: 'Common/User/authorize') do
72
+ # sleep 1
73
+ # end
74
+ def initialize(token:,
75
+ dimensions: nil,
76
+ url: 'https://ingest.signalfx.com',
77
+ open_timeout: 2.0,
78
+ read_timeout: 1.0,
79
+ continue_timeout: 1.0,
80
+ filter: nil,
81
+ application: nil,
82
+ host: nil,
83
+ formatter: nil,
84
+ &block)
85
+
86
+ formatter ||= SemanticLogger::Formatters::Signalfx.new(token: token, dimensions: dimensions)
87
+
88
+ super(
89
+ url: url,
90
+ read_timeout: read_timeout,
91
+ open_timeout: open_timeout,
92
+ continue_timeout: continue_timeout,
93
+ filter: filter,
94
+ application: application,
95
+ host: host,
96
+ formatter: formatter,
97
+ &block
98
+ )
99
+
100
+ @header['X-SF-TOKEN'] = token
101
+ @full_url = "#{url}/#{END_POINT}"
102
+ end
103
+
104
+ def log(log)
105
+ message = formatter.call(log, self)
106
+ logger.trace(message)
107
+ post(message, full_url)
108
+ end
109
+
110
+ # Logs in batches
111
+ def batch(logs)
112
+ message = formatter.batch(logs, self)
113
+ logger.trace(message)
114
+ post(message, full_url)
115
+ end
116
+
117
+ # Only forward log entries that contain metrics.
118
+ def should_log?(log)
119
+ log.metric && meets_log_level?(log) && !filtered?(log)
120
+ end
121
+
122
+ end
123
+
@@ -6,8 +6,10 @@ rescue LoadError
6
6
  end
7
7
 
8
8
  module SemanticLogger
9
- module Metrics
9
+ module Metric
10
10
  class Statsd < Subscriber
11
+ attr_accessor :url
12
+
11
13
  # Create Statsd metrics subscriber
12
14
  #
13
15
  # Parameters:
@@ -20,12 +22,16 @@ module SemanticLogger
20
22
  # Default: udp://localhost:8125
21
23
  #
22
24
  # Example:
23
- # subscriber = SemanticLogger::Metrics::Statsd.new(url: 'udp://localhost:8125')
24
- # SemanticLogger.on_metric(subscriber)
25
- def initialize(options = {})
26
- options = options.dup
27
- @url = options.delete(:url) || 'udp://localhost:8125'
28
- uri = URI.parse(@url)
25
+ # SemanticLogger.add_appender(
26
+ # metric: :statsd,
27
+ # url: 'localhost:8125'
28
+ # )
29
+ def initialize(url: 'udp://localhost:8125')
30
+ @url = url
31
+ end
32
+
33
+ def reopen
34
+ uri = URI.parse(@url)
29
35
  raise('Statsd only supports udp. Example: "udp://localhost:8125"') if uri.scheme != 'udp'
30
36
 
31
37
  @statsd = ::Statsd.new(uri.host, uri.port)
@@ -33,7 +39,7 @@ module SemanticLogger
33
39
  @statsd.namespace = path.sub('/', '') if path != ''
34
40
  end
35
41
 
36
- def call(log)
42
+ def log(log)
37
43
  metric = log.metric
38
44
  if duration = log.duration
39
45
  @statsd.timing(metric, duration)
@@ -47,6 +53,12 @@ module SemanticLogger
47
53
  end
48
54
  end
49
55
 
56
+ # Only forward log entries that contain metrics.
57
+ def should_log?(log)
58
+ # Does not support metrics with dimensions.
59
+ log.metric && !log.dimensions && meets_log_level?(log) && !filtered?(log)
60
+ end
61
+
50
62
  end
51
63
  end
52
64
  end