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
@@ -43,19 +43,18 @@ class SemanticLogger::Appender::Sentry < SemanticLogger::Subscriber
43
43
 
44
44
  # Send an error notification to sentry
45
45
  def log(log)
46
- return false unless should_log?(log)
47
- # Ignore logs coming from Ravent itself
46
+ # Ignore logs coming from Raven itself
48
47
  return false if log.name == 'Raven'
49
48
 
50
49
  context = formatter.call(log, self)
50
+ attrs = {
51
+ level: context.delete(:level),
52
+ extra: context
53
+ }
51
54
  if log.exception
52
55
  context.delete(:exception)
53
- Raven.capture_exception(log.exception, context)
56
+ Raven.capture_exception(log.exception, attrs)
54
57
  else
55
- attrs = {
56
- level: context.delete(:level),
57
- extra: context
58
- }
59
58
  attrs[:extra][:backtrace] = log.backtrace if log.backtrace
60
59
  Raven.capture_message(context[:message], attrs)
61
60
  end
@@ -98,7 +98,7 @@ class SemanticLogger::Appender::Splunk < SemanticLogger::Subscriber
98
98
  # open the handles to resources
99
99
  def reopen
100
100
  # Connect to splunk. Connect is a synonym for creating a Service by hand and calling login.
101
- self.service = Splunk::connect(config)
101
+ self.service = Splunk::connect(config)
102
102
 
103
103
  # The index we are logging to
104
104
  self.service_index = service.indexes[index]
@@ -106,7 +106,6 @@ class SemanticLogger::Appender::Splunk < SemanticLogger::Subscriber
106
106
 
107
107
  # Log the message to Splunk
108
108
  def log(log)
109
- return false unless should_log?(log)
110
109
  event = formatter.call(log, self)
111
110
  service_index.submit(event.delete(:message), event)
112
111
  true
@@ -73,7 +73,7 @@ class SemanticLogger::Appender::SplunkHttp < SemanticLogger::Appender::Http
73
73
  @source_type = source_type
74
74
  @index = index
75
75
 
76
- super(url: url, compress: compress, ssl: ssl, open_timeout: 2.0, read_timeout: open_timeout, continue_timeout: continue_timeout,
76
+ super(url: url, compress: compress, ssl: ssl, read_timeout: read_timeout, open_timeout: open_timeout, continue_timeout: continue_timeout,
77
77
  level: level, formatter: formatter, filter: filter, application: application, host: host, &block)
78
78
 
79
79
  # Put splunk auth token in the header of every HTTP post.
@@ -85,12 +85,11 @@ class SemanticLogger::Appender::SplunkHttp < SemanticLogger::Appender::Http
85
85
  # For splunk format requirements see:
86
86
  # http://dev.splunk.com/view/event-collector/SP-CAAAE6P
87
87
  def call(log, logger)
88
- h = SemanticLogger::Formatters::Raw.new.call(log, logger)
89
- h.delete(:time)
88
+ h = SemanticLogger::Formatters::Raw.new(time_format: :seconds).call(log, logger)
90
89
  message = {
91
90
  source: logger.application,
92
91
  host: logger.host,
93
- time: log.time.utc.to_f,
92
+ time: h.delete(:time),
94
93
  event: h
95
94
  }
96
95
  message[:source_type] = source_type if source_type
@@ -167,7 +167,7 @@ module SemanticLogger
167
167
  ::Syslog.open(application, options, facility)
168
168
  when :tcp
169
169
  # Use the local logger for @remote_syslog so errors with the remote logger can be recorded locally.
170
- @tcp_client_options[:logger] = SemanticLogger::Processor.logger.clone
170
+ @tcp_client_options[:logger] = logger
171
171
  @tcp_client_options[:server] = "#{@server}:#{@port}"
172
172
  @remote_syslog = Net::TCPClient.new(@tcp_client_options)
173
173
  when :udp
@@ -179,8 +179,6 @@ module SemanticLogger
179
179
 
180
180
  # Write the log using the specified protocol and server.
181
181
  def log(log)
182
- return false unless should_log?(log)
183
-
184
182
  case @protocol
185
183
  when :syslog
186
184
  # Since the Ruby Syslog API supports sprintf format strings, double up all existing '%'
@@ -188,12 +188,12 @@ module SemanticLogger
188
188
  on_connect: nil, proxy_server: nil, ssl: nil,
189
189
  level: nil, formatter: nil, filter: nil, application: nil, host: nil, &block
190
190
  )
191
- @separator = separator
192
- @options = {
193
- server: server,
194
- servers: servers,
195
- policy: policy,
196
- buffered: buffered,
191
+ @separator = separator
192
+ @options = {
193
+ server: server,
194
+ servers: servers,
195
+ policy: policy,
196
+ buffered: buffered,
197
197
  #keepalive: keepalive,
198
198
  connect_timeout: connect_timeout,
199
199
  read_timeout: read_timeout,
@@ -208,7 +208,7 @@ module SemanticLogger
208
208
  }
209
209
 
210
210
  # Use the internal logger so that errors with remote logging are only written locally.
211
- Net::TCPClient.logger = SemanticLogger::Processor.logger.clone
211
+ Net::TCPClient.logger = logger
212
212
  Net::TCPClient.logger.name = 'Net::TCPClient'
213
213
 
214
214
  super(level: level, formatter: formatter, filter: filter, application: application, host: host, &block)
@@ -223,8 +223,6 @@ module SemanticLogger
223
223
 
224
224
  # Write the log using the specified protocol and server.
225
225
  def log(log)
226
- return false unless should_log?(log)
227
-
228
226
  message = formatter.call(log, self)
229
227
  @tcp_client.retry_on_connection_failure do
230
228
  @tcp_client.write("#{message}#{separator}")
@@ -76,8 +76,6 @@ module SemanticLogger
76
76
 
77
77
  # Write the log using the specified protocol and server.
78
78
  def log(log)
79
- return false unless should_log?(log)
80
-
81
79
  @socket.send(formatter.call(log, self), udp_flags)
82
80
  true
83
81
  end
@@ -54,8 +54,6 @@ module SemanticLogger
54
54
  # trace entries are mapped to debug since :trace is not supported by the
55
55
  # Ruby or Rails Loggers
56
56
  def log(log)
57
- return false unless should_log?(log)
58
-
59
57
  @logger.send(log.level == :trace ? :debug : log.level, formatter.call(log, self))
60
58
  true
61
59
  end
@@ -124,8 +124,16 @@ module SemanticLogger
124
124
  alias_method :benchmark, :measure
125
125
 
126
126
  # Log a thread backtrace
127
- def backtrace(thread: Thread.current, level: :warn, message: 'Backtrace:', payload: nil, metric: nil, metric_amount: 1)
128
- log = Log.new(name, level)
127
+ def backtrace(thread: Thread.current,
128
+ level: :warn,
129
+ message: 'Backtrace:',
130
+ payload: nil,
131
+ metric: nil,
132
+ metric_amount: nil)
133
+
134
+ log = Log.new(name, level)
135
+ return false unless meets_log_level?(log)
136
+
129
137
  backtrace =
130
138
  if thread == Thread.current
131
139
  Log.cleanse_backtrace
@@ -142,8 +150,10 @@ module SemanticLogger
142
150
  message << backtrace.join("\n")
143
151
  end
144
152
 
145
- if log.assign(message: message, backtrace: backtrace, payload: payload, metric: metric, metric_amount: metric_amount) && should_log?(log)
153
+ if log.assign(message: message, backtrace: backtrace, payload: payload, metric: metric, metric_amount: metric_amount) && !filtered?(log)
146
154
  self.log(log)
155
+ else
156
+ false
147
157
  end
148
158
  end
149
159
 
@@ -232,13 +242,16 @@ module SemanticLogger
232
242
  SemanticLogger.named_tags
233
243
  end
234
244
 
235
- protected
236
-
237
245
  # Write log data to underlying data storage
238
246
  def log(log_)
239
247
  raise NotImplementedError.new('Logging Appender must implement #log(log)')
240
248
  end
241
249
 
250
+ # Whether this log entry meets the criteria to be logged by this appender.
251
+ def should_log?(log)
252
+ meets_log_level?(log) && !filtered?(log)
253
+ end
254
+
242
255
  private
243
256
 
244
257
  # Initializer for Abstract Class SemanticLogger::Base
@@ -281,20 +294,21 @@ module SemanticLogger
281
294
  end
282
295
 
283
296
  # Whether to log the supplied message based on the current filter if any
284
- def include_message?(log)
285
- return true if @filter.nil?
297
+ def filtered?(log)
298
+ return false if @filter.nil?
286
299
 
287
300
  if @filter.is_a?(Regexp)
288
- (@filter =~ log.name) != nil
301
+ (@filter =~ log.name) == nil
289
302
  elsif @filter.is_a?(Proc)
290
- @filter.call(log) == true
303
+ @filter.call(log) != true
304
+ else
305
+ raise(ArgumentError, "Unrecognized semantic logger filter: #{@filter.inspect}, must be a Regexp or a Proc")
291
306
  end
292
307
  end
293
308
 
294
- # Whether the log message should be logged for the current logger or appender
295
- def should_log?(log)
296
- # Ensure minimum log level is met, and check filter
297
- (level_index <= (log.level_index || 0)) && include_message?(log)
309
+ # Ensure minimum log level is met
310
+ def meets_log_level?(log)
311
+ (level_index <= (log.level_index || 0))
298
312
  end
299
313
 
300
314
  # Log message at the specified level
@@ -302,12 +316,18 @@ module SemanticLogger
302
316
  log = Log.new(name, level, index)
303
317
  should_log =
304
318
  if payload.nil? && exception.nil? && message.is_a?(Hash)
305
- log.assign(message)
319
+ # Check if someone just logged a hash payload instead of meaning to call semantic logger
320
+ if message.has_key?(:message) || message.has_key?(:payload) || message.has_key?(:exception) || message.has_key?(:metric)
321
+ log.assign(message)
322
+ else
323
+ log.assign_positional(nil, message, nil, &block)
324
+ end
306
325
  else
307
326
  log.assign_positional(message, payload, exception, &block)
308
327
  end
309
328
 
310
- self.log(log) if should_log && include_message?(log)
329
+ # Log level may change during assign due to :on_exception_level
330
+ self.log(log) if should_log && should_log?(log)
311
331
  end
312
332
 
313
333
  # Measure the supplied block and log the message
@@ -346,26 +366,63 @@ module SemanticLogger
346
366
  end
347
367
 
348
368
  # Extract options after block completes so that block can modify any of the options
349
- payload = params[:payload]
369
+ payload = params[:payload]
350
370
 
371
+ # May return false due to elastic logging
351
372
  should_log = log.assign(
352
373
  message: message,
353
374
  payload: payload,
354
375
  min_duration: params[:min_duration] || 0.0,
355
376
  exception: exception,
356
377
  metric: params[:metric],
357
- metric_amount: 1,
378
+ metric_amount: params[:metric_amount],
358
379
  duration: duration,
359
- backtrace: nil,
360
380
  log_exception: params[:log_exception] || :partial,
361
381
  on_exception_level: params[:on_exception_level]
362
382
  )
363
383
 
364
- self.log(log) if should_log && include_message?(log)
384
+ # Log level may change during assign due to :on_exception_level
385
+ self.log(log) if should_log && should_log?(log)
365
386
  raise exception if exception
366
387
  result
367
388
  end
368
389
  end
369
390
 
391
+ # For measuring methods and logging their duration.
392
+ def measure_method(index:,
393
+ level:,
394
+ message:,
395
+ min_duration:,
396
+ metric:,
397
+ log_exception:,
398
+ on_exception_level:,
399
+ &block)
400
+
401
+ # Ignores filter, silence, payload
402
+ exception = nil
403
+ start = Time.now
404
+ begin
405
+ yield
406
+ rescue Exception => exc
407
+ exception = exc
408
+ ensure
409
+ log = Log.new(name, level, index)
410
+ # May return false due to elastic logging
411
+ should_log = log.assign(
412
+ message: message,
413
+ min_duration: min_duration,
414
+ exception: exception,
415
+ metric: metric,
416
+ duration: 1000.0 * (Time.now - start),
417
+ log_exception: log_exception,
418
+ on_exception_level: on_exception_level
419
+ )
420
+
421
+ # Log level may change during assign due to :on_exception_level
422
+ log(log) if should_log && should_log?(log)
423
+ raise exception if exception
424
+ end
425
+ end
426
+
370
427
  end
371
428
  end
@@ -0,0 +1,37 @@
1
+ module SemanticLogger
2
+ module Formatters
3
+ # @formatter:off
4
+ autoload :Base, 'semantic_logger/formatters/base'
5
+ autoload :Color, 'semantic_logger/formatters/color'
6
+ autoload :Default, 'semantic_logger/formatters/default'
7
+ autoload :Json, 'semantic_logger/formatters/json'
8
+ autoload :Raw, 'semantic_logger/formatters/raw'
9
+ autoload :OneLine, 'semantic_logger/formatters/one_line'
10
+ autoload :Signalfx, 'semantic_logger/formatters/signalfx'
11
+ autoload :Syslog, 'semantic_logger/formatters/syslog'
12
+ # @formatter:on
13
+
14
+ # Return formatter that responds to call.
15
+ #
16
+ # Supports formatter supplied as:
17
+ # - Symbol
18
+ # - Hash ( Symbol => { options })
19
+ # - Instance of any of SemanticLogger::Formatters
20
+ # - Proc
21
+ # - Any object that responds to :call
22
+ def self.factory(formatter)
23
+ case
24
+ when formatter.is_a?(Symbol)
25
+ SemanticLogger::Utils.constantize_symbol(formatter, 'SemanticLogger::Formatters').new
26
+ when formatter.is_a?(Hash) && formatter.size > 0
27
+ fmt, options = formatter.first
28
+ SemanticLogger::Utils.constantize_symbol(fmt.to_sym, 'SemanticLogger::Formatters').new(options)
29
+ when formatter.respond_to?(:call)
30
+ formatter
31
+ else
32
+ raise(ArgumentError, "Unknown formatter: #{formatter.inspect}")
33
+ end
34
+ end
35
+
36
+ end
37
+ end
@@ -9,7 +9,7 @@ module SemanticLogger
9
9
  PRECISION =
10
10
  if defined?(JRuby)
11
11
  if (JRUBY_VERSION.to_f >= 9.1)
12
- maint = JRUBY_VERSION.match(/\A\d\.\d\.(\d)\./)[1].to_i
12
+ maint = JRUBY_VERSION.match(/\A\d+\.\d+\.(\d+)\./)[1].to_i
13
13
  (maint >= 8) || (JRUBY_VERSION.to_f > 9.1) ? 6 : 3
14
14
  else
15
15
  3
@@ -21,8 +21,9 @@ module SemanticLogger
21
21
 
22
22
  # Parameters
23
23
  # time_format: [String|Symbol|nil]
24
- # See Time#strftime for the format of this string
25
- # :iso_8601 Outputs an ISO8601 Formatted timestamp
24
+ # See Time#strftime for the format of this string.
25
+ # :iso_8601 Outputs an ISO8601 Formatted timestamp.
26
+ # :ms Output in miliseconds since epoch.
26
27
  # nil: Returns Empty string for time ( no time is output ).
27
28
  # Default: '%Y-%m-%d %H:%M:%S.%6N'
28
29
  def initialize(time_format: TIME_FORMAT, log_host: true, log_application: true)
@@ -43,6 +44,12 @@ module SemanticLogger
43
44
  case time_format
44
45
  when :iso_8601
45
46
  time.utc.iso8601(PRECISION)
47
+ when :ms
48
+ (time.to_f * 1_000).to_i
49
+ when :none
50
+ time
51
+ when :seconds
52
+ time.to_f
46
53
  when nil
47
54
  ''
48
55
  else
@@ -3,12 +3,8 @@ module SemanticLogger
3
3
  module Formatters
4
4
  class Json < Raw
5
5
  # Default JSON time format is ISO8601
6
- def initialize(time_format: :iso_8601, log_host: true, log_application: true)
7
- super(time_format: time_format, log_host: log_host, log_application: log_application)
8
- end
9
-
10
- def time
11
- hash[:timestamp] = format_time(log.time)
6
+ def initialize(time_format: :iso_8601, log_host: true, log_application: true, time_key: :timestamp)
7
+ super(time_format: time_format, log_host: log_host, log_application: log_application, time_key: time_key)
12
8
  end
13
9
 
14
10
  # Returns log messages in JSON format
@@ -0,0 +1,18 @@
1
+ module SemanticLogger
2
+ module Formatters
3
+ # Only output one line for each log entry.
4
+ #
5
+ # Notes:
6
+ # * New lines are stripped from log messages.
7
+ # * Exceptions only include the class and message, the stack trace is not shown.
8
+ class OneLine < Default
9
+ def message
10
+ "-- #{log.message.gsub("\n", '')}" if log.message
11
+ end
12
+
13
+ def exception
14
+ "-- Exception: #{log.exception.class}: #{log.exception.message.gsub("\n", '')}" if log.exception
15
+ end
16
+ end
17
+ end
18
+ end
@@ -4,7 +4,13 @@ module SemanticLogger
4
4
  class Raw < Base
5
5
 
6
6
  # Fields are added by populating this hash.
7
- attr_accessor :hash, :log, :logger
7
+ attr_accessor :hash, :log, :logger, :time_key
8
+
9
+ # By default Raw formatter does not reformat the time
10
+ def initialize(time_format: :none, log_host: true, log_application: true, time_key: :time)
11
+ @time_key = time_key
12
+ super(time_format: time_format, log_host: log_host, log_application: log_application)
13
+ end
8
14
 
9
15
  # Host name
10
16
  def host
@@ -18,7 +24,7 @@ module SemanticLogger
18
24
 
19
25
  # Date & time
20
26
  def time
21
- hash[:time] = log.time
27
+ hash[time_key] = format_time(log.time)
22
28
  end
23
29
 
24
30
  # Log level
@@ -0,0 +1,169 @@
1
+ require 'json'
2
+ module SemanticLogger
3
+ module Formatters
4
+ class Signalfx < Base
5
+ attr_accessor :token, :dimensions, :hash, :log, :logger, :gauge_name, :counter_name, :environment
6
+
7
+ def initialize(token:,
8
+ dimensions: nil,
9
+ log_host: true,
10
+ log_application: true,
11
+ gauge_name: 'Application.average',
12
+ counter_name: 'Application.counter',
13
+ environment: true)
14
+
15
+ @token = token
16
+ @dimensions = dimensions.map(&:to_sym) if dimensions
17
+ @gauge_name = gauge_name
18
+ @counter_name = counter_name
19
+
20
+ if environment == true
21
+ @environment = defined?(Rails) ? Rails.env : ENV['RAILS_ENV'] || ENV['RACK_ENV'] || 'development'
22
+ elsif environment
23
+ @environment = environment
24
+ end
25
+
26
+ super(time_format: :ms, log_host: log_host, log_application: log_application)
27
+ end
28
+
29
+ # Create SignalFx friendly metric.
30
+ # Strip leading '/'
31
+ # Convert remaining '/' to '.'
32
+ def metric
33
+ if log.dimensions
34
+ name = log.metric.to_s.sub(/\A\/+/, '')
35
+ name.gsub!('/', '.')
36
+ hash[:metric] = name
37
+ else
38
+ # Extract class and action from metric name
39
+ name = log.metric.to_s.sub(/\A\/+/, '')
40
+ names = name.split('/')
41
+ h = (hash[:dimensions] ||= {})
42
+ if names.size > 1
43
+ h[:action] = names.pop
44
+ h[:class] = names.join('::')
45
+ else
46
+ h[:class] = 'Unknown'
47
+ h[:action] = names.first || log.metric
48
+ end
49
+
50
+ hash[:metric] = log.duration ? gauge_name : counter_name
51
+ end
52
+ end
53
+
54
+ # Date & time
55
+ def time
56
+ # 1 second resolution, represented as ms.
57
+ hash[:timestamp] = log.time.to_i * 1000
58
+ end
59
+
60
+ # Value of this metric
61
+ def value
62
+ hash[:value] = log.metric_amount || log.duration || 1
63
+ end
64
+
65
+ # Dimensions for this metric
66
+ def format_dimensions
67
+ h = (hash[:dimensions] ||= {})
68
+ if log.dimensions
69
+ log.dimensions.each_pair do |name, value|
70
+ value = value.to_s
71
+ h[name] = value unless value.empty?
72
+ end
73
+ else
74
+ log.named_tags.each_pair do |name, value|
75
+ name = name.to_sym
76
+ value = value.to_s
77
+ next if value.empty?
78
+ h[name] = value if dimensions && dimensions.include?(name)
79
+ end
80
+ end
81
+ h[:host] = logger.host if log_host && logger.host
82
+ h[:application] = logger.application if log_application && logger.application
83
+ h[:environment] = environment if environment
84
+ end
85
+
86
+ # Returns [Hash] log message in Signalfx format.
87
+ def call(log, logger)
88
+ self.hash = {}
89
+ self.log = log
90
+ self.logger = logger
91
+
92
+ metric; time; value; format_dimensions
93
+
94
+ # gauge, counter, or cumulative_counter
95
+ data = {}
96
+ if log.duration
97
+ data[:gauge] = [hash]
98
+ # Also send a count metric whenever it is a gauge so that it can be counted.
99
+ unless log.dimensions
100
+ count_hash = hash.dup
101
+ count_hash[:value] = log.metric_amount || 1
102
+ count_hash[:metric] = counter_name
103
+ data[:counter] = [count_hash]
104
+ end
105
+ else
106
+ data[:counter] = [hash]
107
+ end
108
+
109
+ data.to_json
110
+ end
111
+
112
+ # Returns [Hash] a batch of log messages.
113
+ # Signalfx has a minimum resolution of 1 second.
114
+ # Metrics of the same type, time (second), and dimensions can be aggregated together.
115
+ def batch(logs, logger)
116
+ self.logger = logger
117
+
118
+ data = {}
119
+ logs.each do |log|
120
+ self.hash = {}
121
+ self.log = log
122
+
123
+ metric; time; value; format_dimensions
124
+
125
+ if log.duration
126
+ gauges = (data[:gauge] ||= [])
127
+ add_gauge(gauges, hash)
128
+
129
+ # Also send a count metric whenever it is a gauge so that it can be counted.
130
+ unless log.dimensions
131
+ count_hash = hash.dup
132
+ count_hash[:value] = log.metric_amount || 1
133
+ count_hash[:metric] = counter_name
134
+ counters = (data[:counter] ||= [])
135
+ add_counter(counters, count_hash)
136
+ end
137
+ else
138
+ counters = (data[:counter] ||= [])
139
+ add_counter(counters, hash)
140
+ end
141
+ end
142
+
143
+ data.to_json
144
+ end
145
+
146
+ private
147
+
148
+ def add_gauge(gauges, metric)
149
+ gauges << metric
150
+ end
151
+
152
+ # Sum counters with the same time (second), name, and dimensions.
153
+ def add_counter(counters, metric)
154
+ existing = find_match(counters, metric)
155
+ existing ? existing[:value] += metric[:value] : counters << metric
156
+ end
157
+
158
+ # Find Metrics with the same timestamp, metric name, and dimensions.
159
+ def find_match(list, metric)
160
+ list.find do |item|
161
+ (item[:timestamp] == metric[:timestamp]) &&
162
+ (item[:metric] == metric[:metric]) &&
163
+ (item[:dimensions] == metric[:dimensions])
164
+ end
165
+ end
166
+
167
+ end
168
+ end
169
+ end