semantic_logger 4.1.1 → 4.2.0

Sign up to get free protection for your applications and to get access to all the features.
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