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
@@ -15,15 +15,11 @@ require 'date'
15
15
  # url: 'http://localhost:9200'
16
16
  # )
17
17
  class SemanticLogger::Appender::Elasticsearch < SemanticLogger::Subscriber
18
- attr_accessor :url, :index, :type, :client, :flush_interval, :timeout_interval, :batch_size
18
+ attr_accessor :url, :index, :type, :client, :flush_interval, :timeout_interval, :batch_size, :elasticsearch_args
19
19
 
20
20
  # Create Elasticsearch appender over persistent HTTP(S)
21
21
  #
22
22
  # Parameters:
23
- # url: [String]
24
- # Fully qualified address to the Elasticsearch service.
25
- # Default: 'http://localhost:9200'
26
- #
27
23
  # index: [String]
28
24
  # Prefix of the index to store the logs in Elasticsearch.
29
25
  # The final index appends the date so that indexes are used per day.
@@ -34,18 +30,6 @@ class SemanticLogger::Appender::Elasticsearch < SemanticLogger::Subscriber
34
30
  # Document type to associate with logs when they are written.
35
31
  # Default: 'log'
36
32
  #
37
- # batch_size: [Fixnum]
38
- # Size of list when sending to Elasticsearch. May be smaller if flush is triggered early.
39
- # Default: 500
40
- #
41
- # flush_interval: [Fixnum]
42
- # Seconds to wait before attempting a flush to Elasticsearch. If no messages queued it's a NOOP.
43
- # Default: 1
44
- #
45
- # timeout_interval: [Fixnum]
46
- # Seconds to allow the Elasticsearch client to flush the bulk message.
47
- # Default: 10
48
- #
49
33
  # level: [:trace | :debug | :info | :warn | :error | :fatal]
50
34
  # Override the log level for this appender.
51
35
  # Default: SemanticLogger.default_level
@@ -68,88 +52,136 @@ class SemanticLogger::Appender::Elasticsearch < SemanticLogger::Subscriber
68
52
  # application: [String]
69
53
  # Name of this application to appear in log messages.
70
54
  # Default: SemanticLogger.application
71
- def initialize(url: 'http://localhost:9200', index: 'semantic_logger', type: 'log', flush_interval: 1, timeout_interval: 10, batch_size: 500,
72
- level: nil, formatter: nil, filter: nil, application: nil, host: nil, &block)
73
-
74
- @url = url
75
- @index = index
76
- @type = type
77
- @flush_interval = flush_interval
78
- @timeout_interval = timeout_interval
79
- @batch_size = batch_size
80
-
81
- @messages_mutex = Mutex.new
82
- @messages = Array.new
55
+ #
56
+ # Elasticsearch Parameters:
57
+ # url: [String]
58
+ # Fully qualified address to the Elasticsearch service.
59
+ # Default: 'http://localhost:9200'
60
+ #
61
+ # hosts: [String|Hash|Array]
62
+ # Single host passed as a String or Hash, or multiple hosts
63
+ # passed as an Array; `host` or `url` keys are also valid.
64
+ # Note:
65
+ # :url above is ignored when supplying this option.
66
+ #
67
+ # resurrect_after [Float]
68
+ # After how many seconds a dead connection should be tried again.
69
+ #
70
+ # reload_connections [true|false|Integer]
71
+ # Reload connections after X requests.
72
+ # Default: false
73
+ #
74
+ # randomize_hosts [true|false]
75
+ # Shuffle connections on initialization and reload.
76
+ # Default: false
77
+ #
78
+ # sniffer_timeout [Integer]
79
+ # Timeout for reloading connections in seconds.
80
+ # Default: 1
81
+ #
82
+ # retry_on_failure [true|false|Integer]
83
+ # Retry X times when request fails before raising and exception.
84
+ # Default: false
85
+ #
86
+ # retry_on_status [Array<Number>]
87
+ # Retry when specific status codes are returned.
88
+ #
89
+ # reload_on_failure [true|false]
90
+ # Reload connections after failure.
91
+ # Default: false
92
+ #
93
+ # request_timeout [Integer]
94
+ # The request timeout to be passed to transport in options.
95
+ #
96
+ # adapter [Symbol]
97
+ # A specific adapter for Faraday (e.g. `:patron`)
98
+ #
99
+ # transport_options [Hash]
100
+ # Options to be passed to the `Faraday::Connection` constructor.
101
+ #
102
+ # transport_class [Constant]
103
+ # A specific transport class to use, will be initialized by
104
+ # the client and passed hosts and all arguments.
105
+ #
106
+ # transport [Object]
107
+ # A specific transport instance.
108
+ #
109
+ # serializer_class [Constant]
110
+ # A specific serializer class to use, will be initialized by
111
+ # the transport and passed the transport instance.
112
+ #
113
+ # selector [Elasticsearch::Transport::Transport::Connections::Selector::Base]
114
+ # An instance of selector strategy derived from `Elasticsearch::Transport::Transport::Connections::Selector::Base`.
115
+ #
116
+ # send_get_body_as [String]
117
+ # Specify the HTTP method to use for GET requests with a body.
118
+ # Default: 'GET'
119
+ def initialize(url: 'http://localhost:9200',
120
+ index: 'semantic_logger',
121
+ type: 'log',
122
+ level: nil,
123
+ formatter: nil,
124
+ filter: nil,
125
+ application: nil,
126
+ host: nil,
127
+ **elasticsearch_args,
128
+ &block)
129
+
130
+ @url = url
131
+ @index = index
132
+ @type = type
133
+ @elasticsearch_args = elasticsearch_args.dup
134
+ @elasticsearch_args[:url] = url if url && !elasticsearch_args[:hosts]
135
+ @elasticsearch_args[:logger] = logger
83
136
 
84
137
  super(level: level, formatter: formatter, filter: filter, application: application, host: host, &block)
85
138
  reopen
86
139
  end
87
140
 
88
141
  def reopen
89
- @client = Elasticsearch::Client.new(url: url, logger: SemanticLogger::Processor.logger.clone)
90
-
91
- @messages_mutex.synchronize { @messages = [] }
92
-
93
- @flush_task = Concurrent::TimerTask.new(execution_interval: flush_interval, timeout_interval: timeout_interval) do
94
- flush
95
- end.execute
96
- end
97
-
98
- def close
99
- @flush_task.shutdown if @flush_task
100
- @flush_task = nil
101
- # No api to close connections in the elasticsearch client!
102
- #@client.close if @client
103
- #@client = nil
142
+ @client = Elasticsearch::Client.new(@elasticsearch_args)
104
143
  end
105
144
 
106
- def call(log, logger)
107
- h = SemanticLogger::Formatters::Raw.new.call(log, logger)
108
- h.delete(:time)
109
- h[:timestamp] = log.time.utc.iso8601(SemanticLogger::Formatters::Base::PRECISION)
110
- h
145
+ # Log to the index for today
146
+ def log(log)
147
+ bulk_payload = formatter.call(log, self)
148
+ write_to_elasticsearch([bulk_index(log), bulk_payload])
149
+ true
111
150
  end
112
151
 
113
- def flush
114
- collected_messages = nil
115
- @messages_mutex.synchronize do
116
- if @messages.length > 0
117
- collected_messages = @messages
118
- @messages = []
152
+ def batch(logs)
153
+ messages = []
154
+ day = nil
155
+ logs.each do |log|
156
+ # Only write the bulk index once per day per batch. Supports mixed dates in a batch.
157
+ if log.time.day != day
158
+ messages << bulk_index(log)
159
+ day = log.time.day
119
160
  end
161
+ messages << formatter.call(log, self)
120
162
  end
121
163
 
122
- if collected_messages
123
- bulk_result = @client.bulk(body: collected_messages)
124
- if bulk_result["errors"]
125
- failed = bulk_result["items"].select { |x| x["status"] != 201 }
126
- SemanticLogger::Processor.logger.error("ElasticSearch: Write failed. Messages discarded. : #{failed}")
127
- end
128
- end
129
- rescue Exception => exc
130
- SemanticLogger::Processor.logger.error('ElasticSearch: Failed to bulk insert log messages', exc)
164
+ write_to_elasticsearch(messages)
165
+ true
131
166
  end
132
167
 
133
- # Log to the index for today
134
- def log(log)
135
- return false unless should_log?(log)
136
-
137
- daily_index = log.time.strftime("#{@index}-%Y.%m.%d")
168
+ private
138
169
 
139
- bulk_index = {'index' => {'_index' => daily_index, '_type' => @type}}
140
- bulk_payload = formatter.call(log, self)
141
-
142
- enqueue(bulk_index, bulk_payload)
170
+ def write_to_elasticsearch(messages)
171
+ bulk_result = @client.bulk(body: messages)
172
+ if bulk_result["errors"]
173
+ failed = bulk_result["items"].select { |x| x["status"] != 201 }
174
+ logger.error("ElasticSearch: Write failed. Messages discarded. : #{failed}")
175
+ end
143
176
  end
144
177
 
145
- def enqueue(bulk_index, bulk_payload)
146
- messages_len =
147
- @messages_mutex.synchronize do
148
- @messages.push(bulk_index)
149
- @messages.push(bulk_payload)
150
- @messages.length
151
- end
178
+ def bulk_index(log)
179
+ daily_index = log.time.strftime("#{index}-%Y.%m.%d")
180
+ {'index' => {'_index' => daily_index, '_type' => type}}
181
+ end
152
182
 
153
- flush if messages_len >= batch_size
183
+ def default_formatter
184
+ SemanticLogger::Formatters::Raw.new(time_format: :iso_8601, time_key: :timestamp)
154
185
  end
186
+
155
187
  end
@@ -54,7 +54,7 @@ class SemanticLogger::Appender::ElasticsearchHttp < SemanticLogger::Appender::Ht
54
54
 
55
55
  @index = index
56
56
  @type = type
57
- super(url: url, compress: compress, ssl: ssl, open_timeout: 2.0, read_timeout: open_timeout, continue_timeout: continue_timeout,
57
+ super(url: url, compress: compress, ssl: ssl, read_timeout: read_timeout, open_timeout: open_timeout, continue_timeout: continue_timeout,
58
58
  level: level, formatter: formatter, filter: filter, application: application, host: host, &block)
59
59
 
60
60
  @request_path = "#{@path.end_with?('/') ? @path : "#{@path}/"}#{@index}-%Y.%m.%d"
@@ -63,8 +63,6 @@ class SemanticLogger::Appender::ElasticsearchHttp < SemanticLogger::Appender::Ht
63
63
 
64
64
  # Log to the index for today.
65
65
  def log(log)
66
- return false unless should_log?(log)
67
-
68
66
  post(formatter.call(log, self), log.time.strftime(@logging_path))
69
67
  end
70
68
 
@@ -81,7 +81,7 @@ module SemanticLogger
81
81
  def reopen
82
82
  return unless @file_name
83
83
 
84
- @log = open(@file_name, (::File::WRONLY | ::File::APPEND | ::File::CREAT))
84
+ @log = open(@file_name, (::File::WRONLY | ::File::APPEND | ::File::CREAT))
85
85
  # Force all log entries to write immediately without buffering
86
86
  # Allows multiple processes to write to the same log file simultaneously
87
87
  @log.sync = true
@@ -93,8 +93,6 @@ module SemanticLogger
93
93
  # trace entries are mapped to debug since :trace is not supported by the
94
94
  # Ruby or Rails Loggers
95
95
  def log(log)
96
- return false unless should_log?(log)
97
-
98
96
  # Since only one appender thread will be writing to the file at a time
99
97
  # it is not necessary to protect access to the file with a semaphore
100
98
  # Allow this logger to filter out log levels lower than it's own
@@ -110,11 +110,9 @@ class SemanticLogger::Appender::Graylog < SemanticLogger::Subscriber
110
110
 
111
111
  # Returns [Hash] of parameters to send
112
112
  def call(log, logger)
113
- h = SemanticLogger::Formatters::Raw.new.call(log, logger)
114
- h.delete(:time)
113
+ h = default_formatter.call(log, logger)
115
114
 
116
115
  h[:short_message] = h.delete(:message) || log.exception.message
117
- h[:timestamp] = log.time.utc.to_f
118
116
  h[:level] = logger.level_map[log.level]
119
117
  h[:level_str] = log.level.to_s
120
118
  h[:duration_str] = h.delete(:duration)
@@ -123,10 +121,13 @@ class SemanticLogger::Appender::Graylog < SemanticLogger::Subscriber
123
121
 
124
122
  # Forward log messages
125
123
  def log(log)
126
- return false unless should_log?(log)
127
-
128
124
  notifier.notify!(formatter.call(log, self))
129
125
  true
130
126
  end
131
127
 
128
+ private
129
+
130
+ def default_formatter
131
+ SemanticLogger::Formatters::Raw.new(time_format: :seconds, time_key: :timestamp)
132
+ end
132
133
  end
@@ -41,8 +41,6 @@ class SemanticLogger::Appender::Honeybadger < SemanticLogger::Subscriber
41
41
 
42
42
  # Send an error notification to honeybadger
43
43
  def log(log)
44
- return false unless should_log?(log)
45
-
46
44
  context = formatter.call(log, self)
47
45
  if log.exception
48
46
  context.delete(:exception)
@@ -2,6 +2,7 @@ require 'net/http'
2
2
  require 'uri'
3
3
  require 'socket'
4
4
  require 'json'
5
+ require 'openssl'
5
6
 
6
7
  # Log to any HTTP(S) server that accepts log messages in JSON form
7
8
  #
@@ -16,7 +17,7 @@ require 'json'
16
17
  # url: 'http://localhost:8088/path'
17
18
  # )
18
19
  class SemanticLogger::Appender::Http < SemanticLogger::Subscriber
19
- attr_accessor :username, :application, :host, :compress, :header,
20
+ attr_accessor :username, :compress, :header,
20
21
  :open_timeout, :read_timeout, :continue_timeout
21
22
  attr_reader :http, :url, :server, :port, :path, :ssl_options
22
23
 
@@ -77,8 +78,20 @@ class SemanticLogger::Appender::Http < SemanticLogger::Subscriber
77
78
  #
78
79
  # continue_timeout: [Float]
79
80
  # Default: 1.0
80
- def initialize(url:, compress: false, ssl: {}, username: nil, password: nil, open_timeout: 2.0, read_timeout: 1.0, continue_timeout: 1.0,
81
- level: nil, formatter: nil, filter: nil, application: nil, host: nil, &block)
81
+ def initialize(url:,
82
+ compress: false,
83
+ ssl: {},
84
+ username: nil,
85
+ password: nil,
86
+ open_timeout: 2.0,
87
+ read_timeout: 1.0,
88
+ continue_timeout: 1.0,
89
+ level: nil,
90
+ formatter: nil,
91
+ filter: nil,
92
+ application: nil,
93
+ host: nil,
94
+ &block)
82
95
 
83
96
  @url = url
84
97
  @ssl_options = ssl
@@ -89,10 +102,10 @@ class SemanticLogger::Appender::Http < SemanticLogger::Subscriber
89
102
  @read_timeout = read_timeout
90
103
  @continue_timeout = continue_timeout
91
104
 
105
+ # On Ruby v2.0 and greater, Net::HTTP.new already uses a persistent connection if the server allows it
92
106
  @header = {
93
107
  'Accept' => 'application/json',
94
108
  'Content-Type' => 'application/json',
95
- # On Ruby v2.0 and greater, Net::HTTP.new already uses a persistent connection if the server allows it
96
109
  'Connection' => 'keep-alive',
97
110
  'Keep-Alive' => '300'
98
111
  }
@@ -148,9 +161,9 @@ class SemanticLogger::Appender::Http < SemanticLogger::Subscriber
148
161
 
149
162
  # Forward log messages to HTTP Server
150
163
  def log(log)
151
- return false unless should_log?(log)
152
-
153
- post(formatter.call(log, self))
164
+ message = formatter.call(log, self)
165
+ logger.trace(message)
166
+ post(message)
154
167
  end
155
168
 
156
169
  private
@@ -189,8 +202,7 @@ class SemanticLogger::Appender::Http < SemanticLogger::Subscriber
189
202
  # Process HTTP Request
190
203
  def process_request(request, body = nil)
191
204
  if body
192
- body = compress_data(body) if compress
193
- request.body = body
205
+ request.body = compress ? compress_data(body) : body
194
206
  end
195
207
  request.basic_auth(@username, @password) if @username
196
208
  response = @http.request(request)
@@ -198,9 +210,12 @@ class SemanticLogger::Appender::Http < SemanticLogger::Subscriber
198
210
  true
199
211
  else
200
212
  # Failures are logged to the global semantic logger failsafe logger (Usually stderr or file)
201
- SemanticLogger::Processor.logger.error("Bad HTTP response from: #{url} code: #{response.code}, #{response.body}")
213
+ logger.error("Bad HTTP response from: #{url} code: #{response.code}, #{response.body}")
202
214
  false
203
215
  end
216
+ rescue RuntimeError => exc
217
+ reopen
218
+ raise exc
204
219
  end
205
220
 
206
221
  end
@@ -142,7 +142,7 @@ class SemanticLogger::Appender::Kafka < SemanticLogger::Subscriber
142
142
  ssl_ca_cert: ssl_ca_cert,
143
143
  ssl_client_cert: ssl_client_cert,
144
144
  ssl_client_cert_key: ssl_client_cert_key,
145
- logger: SemanticLogger::Processor.logger.clone
145
+ logger: logger
146
146
  )
147
147
 
148
148
  @producer = @kafka.async_producer(
@@ -160,8 +160,6 @@ class SemanticLogger::Appender::Kafka < SemanticLogger::Subscriber
160
160
 
161
161
  # Forward log messages to Kafka producer thread.
162
162
  def log(log)
163
- return false unless should_log?(log)
164
-
165
163
  json = formatter.call(log, self)
166
164
  @producer.produce(json, topic: topic, partition: partition, partition_key: partition_key, key: key)
167
165
  end
@@ -106,7 +106,7 @@ module SemanticLogger
106
106
  def initialize(uri:, collection_name: 'semantic_logger', write_concern: 0, collection_size: 1024**3, collection_max: nil,
107
107
  level: nil, formatter: nil, filter: nil, host: nil, application: nil, &block)
108
108
 
109
- @client = Mongo::Client.new(uri, logger: SemanticLogger::Processor.logger.clone)
109
+ @client = Mongo::Client.new(uri, logger: logger)
110
110
  @collection_name = collection_name
111
111
  @options = {
112
112
  capped: true,
@@ -161,8 +161,6 @@ module SemanticLogger
161
161
 
162
162
  # Log the message to MongoDB
163
163
  def log(log)
164
- return false unless should_log?(log)
165
-
166
164
  # Insert log entry into Mongo
167
165
  collection.insert_one(formatter.call(log, self))
168
166
  true
@@ -29,7 +29,13 @@ class SemanticLogger::Appender::NewRelic < SemanticLogger::Subscriber
29
29
  # regular expression. All other messages will be ignored.
30
30
  # Proc: Only include log messages where the supplied Proc returns true
31
31
  # The Proc must return true or false.
32
- def initialize(level: :error, formatter: nil, filter: nil, application: nil, host: nil, &block)
32
+ def initialize(level: :error,
33
+ formatter: nil,
34
+ filter: nil,
35
+ application: nil,
36
+ host: nil,
37
+ &block)
38
+
33
39
  super(level: level, formatter: formatter, filter: filter, application: application, host: host, &block)
34
40
  end
35
41
 
@@ -43,8 +49,6 @@ class SemanticLogger::Appender::NewRelic < SemanticLogger::Subscriber
43
49
 
44
50
  # Send an error notification to New Relic
45
51
  def log(log)
46
- return false unless should_log?(log)
47
-
48
52
  # Send error messages as Runtime exceptions
49
53
  exception =
50
54
  if log.exception