semantic_logger 5.1.0 → 5.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.
- checksums.yaml +4 -4
- data/README.md +13 -12
- data/Rakefile +15 -4
- data/docs/api.md +37 -11
- data/docs/appenders.md +123 -39
- data/docs/config.md +115 -13
- data/docs/index.md +31 -5
- data/docs/log.md +19 -11
- data/docs/metrics.md +5 -2
- data/docs/operations.md +63 -17
- data/docs/rails.md +77 -3
- data/docs/security.md +183 -16
- data/docs/testing.md +5 -3
- data/docs/upgrading.md +295 -7
- data/lib/semantic_logger/appender/async.rb +18 -5
- data/lib/semantic_logger/appender/bugsnag.rb +1 -1
- data/lib/semantic_logger/appender/cloudwatch_logs.rb +122 -27
- data/lib/semantic_logger/appender/elasticsearch_base.rb +31 -6
- data/lib/semantic_logger/appender/elasticsearch_http.rb +1 -1
- data/lib/semantic_logger/appender/file.rb +138 -30
- data/lib/semantic_logger/appender/graylog.rb +22 -6
- data/lib/semantic_logger/appender/honeybadger.rb +1 -1
- data/lib/semantic_logger/appender/honeybadger_insights.rb +1 -1
- data/lib/semantic_logger/appender/http.rb +70 -11
- data/lib/semantic_logger/appender/io.rb +1 -1
- data/lib/semantic_logger/appender/kafka.rb +26 -12
- data/lib/semantic_logger/appender/loki.rb +13 -3
- data/lib/semantic_logger/appender/mongodb.rb +20 -1
- data/lib/semantic_logger/appender/new_relic.rb +1 -1
- data/lib/semantic_logger/appender/new_relic_logs.rb +9 -11
- data/lib/semantic_logger/appender/open_telemetry.rb +2 -2
- data/lib/semantic_logger/appender/rabbitmq.rb +6 -1
- data/lib/semantic_logger/appender/sentry.rb +1 -1
- data/lib/semantic_logger/appender/sentry_ruby.rb +91 -17
- data/lib/semantic_logger/appender/splunk.rb +10 -1
- data/lib/semantic_logger/appender/syslog.rb +64 -26
- data/lib/semantic_logger/appender/tcp.rb +6 -1
- data/lib/semantic_logger/appender/udp.rb +11 -1
- data/lib/semantic_logger/appender/wrapper.rb +1 -1
- data/lib/semantic_logger/appenders.rb +9 -1
- data/lib/semantic_logger/base.rb +82 -30
- data/lib/semantic_logger/exception_details.rb +50 -0
- data/lib/semantic_logger/formatters/base.rb +52 -6
- data/lib/semantic_logger/formatters/color.rb +18 -2
- data/lib/semantic_logger/formatters/default.rb +22 -5
- data/lib/semantic_logger/formatters/logfmt.rb +153 -21
- data/lib/semantic_logger/formatters/loki.rb +26 -22
- data/lib/semantic_logger/formatters/one_line.rb +67 -3
- data/lib/semantic_logger/formatters/open_telemetry.rb +16 -17
- data/lib/semantic_logger/formatters/pattern.rb +8 -4
- data/lib/semantic_logger/formatters/raw.rb +4 -5
- data/lib/semantic_logger/formatters/syslog.rb +110 -18
- data/lib/semantic_logger/formatters/syslog_cee.rb +29 -16
- data/lib/semantic_logger/levels.rb +10 -1
- data/lib/semantic_logger/log.rb +183 -37
- data/lib/semantic_logger/loggable.rb +26 -3
- data/lib/semantic_logger/metric/new_relic.rb +1 -1
- data/lib/semantic_logger/metric/signalfx.rb +8 -6
- data/lib/semantic_logger/metric/statsd.rb +15 -7
- data/lib/semantic_logger/queue_processor/request.rb +55 -0
- data/lib/semantic_logger/queue_processor/throttled_counter.rb +46 -0
- data/lib/semantic_logger/queue_processor.rb +165 -66
- data/lib/semantic_logger/semantic_logger.rb +15 -2
- data/lib/semantic_logger/subscriber.rb +96 -10
- data/lib/semantic_logger/sync_processor.rb +11 -0
- data/lib/semantic_logger/test/capture_log_events.rb +8 -5
- data/lib/semantic_logger/utils.rb +244 -16
- data/lib/semantic_logger/version.rb +1 -1
- data/lib/semantic_logger.rb +1 -1
- metadata +21 -5
- data/lib/semantic_logger/core_ext/thread.rb +0 -30
|
@@ -31,7 +31,13 @@ module SemanticLogger
|
|
|
31
31
|
class Http < SemanticLogger::Subscriber
|
|
32
32
|
attr_accessor :username, :compress, :header,
|
|
33
33
|
:open_timeout, :read_timeout, :continue_timeout
|
|
34
|
-
attr_reader :http, :url, :server, :port, :path, :ssl_options, :proxy_url
|
|
34
|
+
attr_reader :http, :url, :server, :port, :path, :query, :ssl_options, :proxy_url
|
|
35
|
+
|
|
36
|
+
# Maximum number of bytes of an error response body to write to the internal logger.
|
|
37
|
+
MAX_LOGGED_RESPONSE_BODY = 512
|
|
38
|
+
|
|
39
|
+
# Describes a valid url in the error raised for an invalid one.
|
|
40
|
+
URL_FORMAT_HINT = "Should be similar to: 'http://hostname:port/path'".freeze
|
|
35
41
|
|
|
36
42
|
# Create HTTP(S) log appender
|
|
37
43
|
#
|
|
@@ -42,6 +48,9 @@ module SemanticLogger
|
|
|
42
48
|
# To enable SSL include https in the URL.
|
|
43
49
|
# Example: https://example.com/some_path
|
|
44
50
|
# verify_mode will default: OpenSSL::SSL::VERIFY_PEER
|
|
51
|
+
# A query string is kept and sent with every request that submits log data,
|
|
52
|
+
# for the log servers that are configured through it.
|
|
53
|
+
# Example: http://example.com/some_path?source=my_app
|
|
45
54
|
#
|
|
46
55
|
# application: [String]
|
|
47
56
|
# Name of this application to appear in log messages.
|
|
@@ -136,11 +145,10 @@ module SemanticLogger
|
|
|
136
145
|
}.merge(header)
|
|
137
146
|
@header["Content-Encoding"] = "gzip" if @compress
|
|
138
147
|
|
|
139
|
-
uri =
|
|
148
|
+
uri = parse_url(@url)
|
|
140
149
|
@server = uri.host
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
end
|
|
150
|
+
# The url is left out of the message, since it can hold a user and password.
|
|
151
|
+
raise(ArgumentError, "Invalid format for :url, it has no host. #{URL_FORMAT_HINT}") unless @server
|
|
144
152
|
|
|
145
153
|
@port = uri.port
|
|
146
154
|
@username = uri.user if !@username && uri.user
|
|
@@ -148,6 +156,11 @@ module SemanticLogger
|
|
|
148
156
|
@path = uri.path
|
|
149
157
|
# Path cannot be empty
|
|
150
158
|
@path = "/" if @path == ""
|
|
159
|
+
# Query string of the url, applied to every request, see #uri_with_query.
|
|
160
|
+
# Kept separate from the path, because subclasses build their own request paths
|
|
161
|
+
# from it. For example Appender::ElasticsearchHttp appends the index name.
|
|
162
|
+
@query = uri.query
|
|
163
|
+
@query = nil if @query == ""
|
|
151
164
|
|
|
152
165
|
if uri.scheme == "https"
|
|
153
166
|
@ssl_options[:use_ssl] = true
|
|
@@ -157,7 +170,7 @@ module SemanticLogger
|
|
|
157
170
|
@port ||= HTTP.http_default_port
|
|
158
171
|
end
|
|
159
172
|
|
|
160
|
-
@proxy_uri =
|
|
173
|
+
@proxy_uri = Utils.parse_url(@proxy_url, option: ":proxy_url") if @proxy_url && @proxy_url != :ENV
|
|
161
174
|
|
|
162
175
|
@http = nil
|
|
163
176
|
|
|
@@ -196,7 +209,7 @@ module SemanticLogger
|
|
|
196
209
|
|
|
197
210
|
# Forward log messages to HTTP Server
|
|
198
211
|
def log(log)
|
|
199
|
-
message =
|
|
212
|
+
message = format_log(log)
|
|
200
213
|
logger.trace(message)
|
|
201
214
|
post(message)
|
|
202
215
|
end
|
|
@@ -204,7 +217,7 @@ module SemanticLogger
|
|
|
204
217
|
# Forward a batch of log messages to the HTTP Server in a single request.
|
|
205
218
|
# Only used when the appender is created with `batch: true`.
|
|
206
219
|
def batch(logs)
|
|
207
|
-
message =
|
|
220
|
+
message = format_batch(logs)
|
|
208
221
|
logger.trace(message)
|
|
209
222
|
post(message)
|
|
210
223
|
end
|
|
@@ -216,8 +229,30 @@ module SemanticLogger
|
|
|
216
229
|
false
|
|
217
230
|
end
|
|
218
231
|
|
|
232
|
+
# The whole batch is formatted into a single request before anything is sent.
|
|
233
|
+
def batch_all_or_nothing?
|
|
234
|
+
true
|
|
235
|
+
end
|
|
236
|
+
|
|
237
|
+
# Returns [String] the scheme, host, and any non-default port of the url. Safe to log.
|
|
238
|
+
# See Utils.redacted_url.
|
|
239
|
+
def redacted_url
|
|
240
|
+
@redacted_url ||= Utils.redacted_url(url)
|
|
241
|
+
end
|
|
242
|
+
|
|
243
|
+
# Only the redacted url, since the instance variables hold the password, the authorization
|
|
244
|
+
# headers, and any credential a subclass adds. See Subscriber#inspect.
|
|
245
|
+
def inspect_details
|
|
246
|
+
"url=#{redacted_url.inspect}"
|
|
247
|
+
end
|
|
248
|
+
|
|
219
249
|
private
|
|
220
250
|
|
|
251
|
+
# Returns [URI] the parsed url, without the url in the error raised for an invalid one.
|
|
252
|
+
def parse_url(url)
|
|
253
|
+
Utils.parse_url(url, hint: URL_FORMAT_HINT)
|
|
254
|
+
end
|
|
255
|
+
|
|
221
256
|
# Use JSON Formatter by default
|
|
222
257
|
def default_formatter
|
|
223
258
|
SemanticLogger::Formatters::Json.new
|
|
@@ -233,22 +268,43 @@ module SemanticLogger
|
|
|
233
268
|
|
|
234
269
|
# HTTP Post
|
|
235
270
|
def post(body, request_uri = path)
|
|
236
|
-
request = Net::HTTP::Post.new(request_uri, @header)
|
|
271
|
+
request = Net::HTTP::Post.new(uri_with_query(request_uri), @header)
|
|
237
272
|
process_request(request, body)
|
|
238
273
|
end
|
|
239
274
|
|
|
240
275
|
# HTTP Put
|
|
241
276
|
def put(body, request_uri = path)
|
|
242
|
-
request = Net::HTTP::Put.new(request_uri, @header)
|
|
277
|
+
request = Net::HTTP::Put.new(uri_with_query(request_uri), @header)
|
|
243
278
|
process_request(request, body)
|
|
244
279
|
end
|
|
245
280
|
|
|
246
281
|
# HTTP Delete
|
|
282
|
+
#
|
|
283
|
+
# The query string of the configured url is _not_ applied here: it configures how
|
|
284
|
+
# the server ingests log data, and servers reject it on a maintenance request.
|
|
285
|
+
# Appender::ElasticsearchHttp#delete_all deletes an index, and Elasticsearch fails
|
|
286
|
+
# the request with a 400 when it carries unrecognized parameters.
|
|
247
287
|
def delete(request_uri = path)
|
|
248
288
|
request = Net::HTTP::Delete.new(request_uri, @header)
|
|
249
289
|
process_request(request)
|
|
250
290
|
end
|
|
251
291
|
|
|
292
|
+
# Applies the query string of the configured url, if any, to the uri of a request
|
|
293
|
+
# that submits log data.
|
|
294
|
+
#
|
|
295
|
+
# Kept out of `path` on purpose: subclasses build their own request paths from it
|
|
296
|
+
# (Appender::ElasticsearchHttp appends the index name and `_doc` to it), and the
|
|
297
|
+
# query has to stay at the end of the request uri regardless.
|
|
298
|
+
#
|
|
299
|
+
# @param request_uri [String] path of the request, `path` by default.
|
|
300
|
+
# @return [String]
|
|
301
|
+
def uri_with_query(request_uri)
|
|
302
|
+
return request_uri unless @query
|
|
303
|
+
|
|
304
|
+
separator = request_uri.include?("?") ? "&" : "?"
|
|
305
|
+
"#{request_uri}#{separator}#{@query}"
|
|
306
|
+
end
|
|
307
|
+
|
|
252
308
|
# Process HTTP Request
|
|
253
309
|
def process_request(request, body = nil)
|
|
254
310
|
if body
|
|
@@ -260,7 +316,10 @@ module SemanticLogger
|
|
|
260
316
|
true
|
|
261
317
|
else
|
|
262
318
|
# Failures are logged to the global semantic logger failsafe logger (Usually stderr or file)
|
|
263
|
-
logger.error(
|
|
319
|
+
logger.error(
|
|
320
|
+
"Bad HTTP response from: #{redacted_url} code: #{response.code}, " \
|
|
321
|
+
"#{response.body.to_s[0, MAX_LOGGED_RESPONSE_BODY].inspect}"
|
|
322
|
+
)
|
|
264
323
|
false
|
|
265
324
|
end
|
|
266
325
|
rescue RuntimeError => e
|
|
@@ -50,7 +50,7 @@ module SemanticLogger
|
|
|
50
50
|
# Since only one appender thread will be writing to the file at a time
|
|
51
51
|
# it is not necessary to protect access to the file with a semaphore
|
|
52
52
|
# Allow this logger to filter out log levels lower than it's own
|
|
53
|
-
@io.write(
|
|
53
|
+
@io.write(format_log(log) << "\n")
|
|
54
54
|
true
|
|
55
55
|
end
|
|
56
56
|
|
|
@@ -23,6 +23,9 @@ require "date"
|
|
|
23
23
|
module SemanticLogger
|
|
24
24
|
module Appender
|
|
25
25
|
class Kafka < SemanticLogger::Subscriber
|
|
26
|
+
SEED_BROKERS_HINT = "Each should be similar to: 'hostname:port' or 'kafka://hostname:port'".freeze
|
|
27
|
+
private_constant :SEED_BROKERS_HINT
|
|
28
|
+
|
|
26
29
|
attr_accessor :seed_brokers, :client_id, :connect_timeout, :socket_timeout,
|
|
27
30
|
:ssl_ca_cert, :ssl_client_cert, :ssl_client_cert_key, :ssl_ca_certs_from_system,
|
|
28
31
|
:delivery_threshold, :delivery_interval, :required_acks,
|
|
@@ -149,17 +152,22 @@ module SemanticLogger
|
|
|
149
152
|
end
|
|
150
153
|
|
|
151
154
|
def reopen
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
155
|
+
# ruby-kafka puts the seed broker, which can hold credentials, in the error it raises for an
|
|
156
|
+
# invalid one: URI::InvalidURIError, or Kafka::Error for an unknown scheme.
|
|
157
|
+
errors = [URI::InvalidURIError, ::Kafka::Error]
|
|
158
|
+
@kafka = Utils.without_url_in_errors(option: ":seed_brokers", hint: SEED_BROKERS_HINT, errors: errors) do
|
|
159
|
+
::Kafka.new(
|
|
160
|
+
seed_brokers: seed_brokers,
|
|
161
|
+
client_id: client_id,
|
|
162
|
+
connect_timeout: connect_timeout,
|
|
163
|
+
socket_timeout: socket_timeout,
|
|
164
|
+
ssl_ca_cert: ssl_ca_cert,
|
|
165
|
+
ssl_client_cert: ssl_client_cert,
|
|
166
|
+
ssl_client_cert_key: ssl_client_cert_key,
|
|
167
|
+
ssl_ca_certs_from_system: ssl_ca_certs_from_system,
|
|
168
|
+
logger: logger
|
|
169
|
+
)
|
|
170
|
+
end
|
|
163
171
|
|
|
164
172
|
@producer = @kafka.async_producer(
|
|
165
173
|
delivery_threshold: delivery_threshold,
|
|
@@ -177,7 +185,7 @@ module SemanticLogger
|
|
|
177
185
|
|
|
178
186
|
# Forward log messages to Kafka producer thread.
|
|
179
187
|
def log(log)
|
|
180
|
-
json =
|
|
188
|
+
json = format_log(log)
|
|
181
189
|
@producer.produce(json, topic: topic, partition: partition, partition_key: partition_key, key: key)
|
|
182
190
|
end
|
|
183
191
|
|
|
@@ -195,6 +203,12 @@ module SemanticLogger
|
|
|
195
203
|
)
|
|
196
204
|
end
|
|
197
205
|
|
|
206
|
+
# Only the topic: the seed brokers can hold credentials, and the options hold the TLS private
|
|
207
|
+
# key and SASL passwords. See Subscriber#inspect.
|
|
208
|
+
def inspect_details
|
|
209
|
+
"topic=#{@topic.inspect}"
|
|
210
|
+
end
|
|
211
|
+
|
|
198
212
|
private
|
|
199
213
|
|
|
200
214
|
attr_reader :producer
|
|
@@ -33,6 +33,8 @@ module SemanticLogger
|
|
|
33
33
|
# url: [String]
|
|
34
34
|
# Define the loki instance URL.
|
|
35
35
|
# Example: https://logs-prod-999.grafana.net
|
|
36
|
+
# A query string is kept and sent with every request.
|
|
37
|
+
# Example: https://logs-prod-999.grafana.net?orgid=my_org
|
|
36
38
|
# Default: nil
|
|
37
39
|
def initialize(url: nil,
|
|
38
40
|
formatter: SemanticLogger::Formatters::Loki.new,
|
|
@@ -40,18 +42,26 @@ module SemanticLogger
|
|
|
40
42
|
path: INGESTION_PATH,
|
|
41
43
|
**args,
|
|
42
44
|
&)
|
|
43
|
-
super(url:
|
|
45
|
+
super(url: self.class.ingestion_url(url, path), formatter: formatter, header: header, **args, &)
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
# Appends the ingestion path to the supplied url, keeping any query string at the
|
|
49
|
+
# end of the url where Appender::Http can parse it out.
|
|
50
|
+
def self.ingestion_url(url, path)
|
|
51
|
+
uri = Utils.parse_url(url.to_s)
|
|
52
|
+
uri.path = "#{uri.path.chomp('/')}/#{path.delete_prefix('/')}"
|
|
53
|
+
uri.to_s
|
|
44
54
|
end
|
|
45
55
|
|
|
46
56
|
def log(log)
|
|
47
|
-
message =
|
|
57
|
+
message = format_log(log)
|
|
48
58
|
logger.trace(message)
|
|
49
59
|
post(message)
|
|
50
60
|
end
|
|
51
61
|
|
|
52
62
|
# Logs in batches
|
|
53
63
|
def batch(logs)
|
|
54
|
-
message =
|
|
64
|
+
message = format_batch(logs)
|
|
55
65
|
logger.trace(message)
|
|
56
66
|
post(message)
|
|
57
67
|
end
|
|
@@ -164,10 +164,29 @@ module SemanticLogger
|
|
|
164
164
|
# Log the message to MongoDB
|
|
165
165
|
def log(log)
|
|
166
166
|
# Insert log entry into Mongo
|
|
167
|
-
collection.insert_one(
|
|
167
|
+
collection.insert_one(format_log(log))
|
|
168
168
|
true
|
|
169
169
|
end
|
|
170
170
|
|
|
171
|
+
# Errors raised for a document that cannot be stored, which retrying cannot insert either.
|
|
172
|
+
# BSON raises BSON::Error, ArgumentError (a key with a NUL byte), or RangeError (an Integer too
|
|
173
|
+
# large for 64 bits) while the document is serialized, before anything is sent, and the driver
|
|
174
|
+
# raises Mongo::Error::MaxBSONSize for a document larger than the server accepts.
|
|
175
|
+
DOCUMENT_ERRORS = [BSON::Error, ArgumentError, RangeError, Mongo::Error::MaxBSONSize].freeze
|
|
176
|
+
|
|
177
|
+
# Except BSON::Error::BSONDecodeError, which comes from reading the server's reply rather than
|
|
178
|
+
# from the document, so it is retried like any other error from the server.
|
|
179
|
+
def message_error?(exception)
|
|
180
|
+
return false if exception.is_a?(BSON::Error::BSONDecodeError)
|
|
181
|
+
|
|
182
|
+
super || DOCUMENT_ERRORS.any? { |error| exception.is_a?(error) }
|
|
183
|
+
end
|
|
184
|
+
|
|
185
|
+
# Only the collection name: the uri can hold a user and password. See Subscriber#inspect.
|
|
186
|
+
def inspect_details
|
|
187
|
+
"collection=#{@collection_name.inspect}"
|
|
188
|
+
end
|
|
189
|
+
|
|
171
190
|
private
|
|
172
191
|
|
|
173
192
|
def default_formatter
|
|
@@ -64,7 +64,7 @@ module SemanticLogger
|
|
|
64
64
|
# For more documentation on the NewRelic::Agent.notice_error method see:
|
|
65
65
|
# http://rubydoc.info/github/newrelic/rpm/NewRelic/Agent#notice_error-instance_method
|
|
66
66
|
# and https://docs.newrelic.com/docs/ruby/ruby-agent-api
|
|
67
|
-
::NewRelic::Agent.notice_error(exception,
|
|
67
|
+
::NewRelic::Agent.notice_error(exception, format_log(log))
|
|
68
68
|
true
|
|
69
69
|
end
|
|
70
70
|
end
|
|
@@ -57,17 +57,15 @@ module SemanticLogger
|
|
|
57
57
|
|
|
58
58
|
# Send an error notification to New Relic
|
|
59
59
|
def log(log)
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
warn("Unexpected error while logging to New Relic: #{e.message}")
|
|
70
|
-
end
|
|
60
|
+
message = format_log(log) # Generate the structured log
|
|
61
|
+
json_message = Utils.to_json(message) # Convert the log to JSON, repairing any non UTF-8 data
|
|
62
|
+
level = log.level.to_s.upcase # Determine the log level
|
|
63
|
+
self.class.log_newrelic(json_message, level)
|
|
64
|
+
true
|
|
65
|
+
rescue StandardError => e
|
|
66
|
+
# Reported to the internal logger: this appender's own #warn would log the error through
|
|
67
|
+
# this method again, recursing until SystemStackError.
|
|
68
|
+
logger.warn("Could not log to New Relic", e)
|
|
71
69
|
true
|
|
72
70
|
end
|
|
73
71
|
|
|
@@ -48,7 +48,7 @@ module SemanticLogger
|
|
|
48
48
|
def log(log)
|
|
49
49
|
# return log_metric(log) if metrics && log.metric_only?
|
|
50
50
|
|
|
51
|
-
body =
|
|
51
|
+
body = format_log(log)
|
|
52
52
|
level = body.delete(:level)
|
|
53
53
|
level_index = body.delete(:level_index)
|
|
54
54
|
time = body.delete(:time)
|
|
@@ -89,7 +89,7 @@ module SemanticLogger
|
|
|
89
89
|
# For logging metrics only log events.
|
|
90
90
|
# def log_metric(log)
|
|
91
91
|
# puts "**** TODO: Metric Only Event ****"
|
|
92
|
-
# ap
|
|
92
|
+
# ap format_log(log)
|
|
93
93
|
# ap log.payload
|
|
94
94
|
# true
|
|
95
95
|
# end
|
|
@@ -107,7 +107,7 @@ module SemanticLogger
|
|
|
107
107
|
end
|
|
108
108
|
|
|
109
109
|
def log(log)
|
|
110
|
-
queue.publish(
|
|
110
|
+
queue.publish(format_log(log))
|
|
111
111
|
end
|
|
112
112
|
|
|
113
113
|
def flush
|
|
@@ -122,6 +122,11 @@ module SemanticLogger
|
|
|
122
122
|
def queue
|
|
123
123
|
@queue ||= @channel.queue(@queue_name)
|
|
124
124
|
end
|
|
125
|
+
|
|
126
|
+
# Only the queue name: the connection options hold the password. See Subscriber#inspect.
|
|
127
|
+
def inspect_details
|
|
128
|
+
"queue_name=#{@queue_name.inspect}"
|
|
129
|
+
end
|
|
125
130
|
end
|
|
126
131
|
end
|
|
127
132
|
end
|
|
@@ -12,6 +12,15 @@ end
|
|
|
12
12
|
module SemanticLogger
|
|
13
13
|
module Appender
|
|
14
14
|
class SentryRuby < SemanticLogger::Subscriber
|
|
15
|
+
# Keys of the named tags or payload that set the Sentry user, mapped to its fields.
|
|
16
|
+
USER_KEYS = {user_id: :id, username: :username, user_email: :email, ip_address: :ip_address}.freeze
|
|
17
|
+
|
|
18
|
+
# The types of value taken from the payload for a field of the Sentry user.
|
|
19
|
+
USER_VALUE_TYPES = [String, Symbol, Integer].freeze
|
|
20
|
+
|
|
21
|
+
# The types of value accepted in a fingerprint, which Sentry turns into Strings.
|
|
22
|
+
FINGERPRINT_PART_TYPES = [String, Symbol, Numeric].freeze
|
|
23
|
+
|
|
15
24
|
# Create Appender
|
|
16
25
|
#
|
|
17
26
|
# Parameters
|
|
@@ -54,16 +63,19 @@ module SemanticLogger
|
|
|
54
63
|
# Ignore logs coming from Sentry itself
|
|
55
64
|
return false if log.name == "Sentry"
|
|
56
65
|
|
|
57
|
-
context =
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
66
|
+
context = format_log(log)
|
|
67
|
+
# Copies, since the payload and named tags are the log entry's own, which every other
|
|
68
|
+
# appender writes after this one, and the payload can be the application's own Hash. The
|
|
69
|
+
# keys taken out below must only be left out of what is sent to Sentry.
|
|
70
|
+
payload = payload_copy(context.delete(:payload))
|
|
71
|
+
named_tags = (context[:named_tags] || {}).dup
|
|
72
|
+
context[:named_tags] = named_tags
|
|
73
|
+
transaction_name = named_tags.delete(:transaction_name)
|
|
74
|
+
fingerprint = extract_fingerprint!(named_tags) || extract_fingerprint!(payload, from_payload: true)
|
|
61
75
|
|
|
62
76
|
user = extract_user!(named_tags, payload)
|
|
63
77
|
tags = extract_tags!(context)
|
|
64
78
|
|
|
65
|
-
fingerprint = payload.delete(:fingerprint)
|
|
66
|
-
|
|
67
79
|
::Sentry.with_scope do |scope|
|
|
68
80
|
scope.set_user(user) if user
|
|
69
81
|
scope.set_level(context.delete(:level)) if context[:level]
|
|
@@ -100,26 +112,88 @@ module SemanticLogger
|
|
|
100
112
|
# Any additional value nested in a :user key will be added, provided any of
|
|
101
113
|
# the above keys is already present.
|
|
102
114
|
#
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
115
|
+
# The payload can hold untrusted data, such as a request body, so taking the user from it is
|
|
116
|
+
# deprecated, and the named tags win: a payload field is only taken when the named tags do not
|
|
117
|
+
# set it, and its value is a String, Symbol, or Integer. A payload value that is not taken is
|
|
118
|
+
# left where it is, and sent to Sentry as data.
|
|
119
|
+
def extract_user!(named_tags, payload)
|
|
106
120
|
user = {}
|
|
107
121
|
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
user[target_key] = value
|
|
122
|
+
USER_KEYS.each do |source_key, target_key|
|
|
123
|
+
value = named_tags.delete(source_key)
|
|
124
|
+
if value
|
|
125
|
+
user[target_key] = value
|
|
126
|
+
elsif user_value?(payload[source_key])
|
|
127
|
+
user[target_key] = payload.delete(source_key)
|
|
128
|
+
payload_context_deprecated
|
|
112
129
|
end
|
|
113
130
|
end
|
|
114
131
|
|
|
115
132
|
return if user.empty?
|
|
116
133
|
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
134
|
+
user.merge!(named_tags.delete(:user)) if named_tags[:user].is_a?(Hash)
|
|
135
|
+
merge_payload_user!(user, payload)
|
|
136
|
+
user
|
|
137
|
+
end
|
|
138
|
+
|
|
139
|
+
# Adds the fields of a :user Hash in the payload that the user does not have yet, when each of
|
|
140
|
+
# its keys is a Symbol or String, and each of its values a String, Symbol, or Integer.
|
|
141
|
+
def merge_payload_user!(user, payload)
|
|
142
|
+
extras = payload[:user]
|
|
143
|
+
return unless extras.is_a?(Hash) && extras.all? { |key, value| user_key?(key) && user_value?(value) }
|
|
144
|
+
|
|
145
|
+
payload.delete(:user)
|
|
146
|
+
payload_context_deprecated
|
|
147
|
+
extras.each do |key, value|
|
|
148
|
+
user[key.to_sym] = value unless user.key?(key.to_sym) || user.key?(key.to_s)
|
|
120
149
|
end
|
|
150
|
+
end
|
|
121
151
|
|
|
122
|
-
|
|
152
|
+
def user_key?(key)
|
|
153
|
+
key.is_a?(Symbol) || key.is_a?(String)
|
|
154
|
+
end
|
|
155
|
+
|
|
156
|
+
def user_value?(value)
|
|
157
|
+
USER_VALUE_TYPES.any? { |type| value.is_a?(type) }
|
|
158
|
+
end
|
|
159
|
+
|
|
160
|
+
# Returns [Array<String>] the fingerprint taken from the named tags or payload, an Array of
|
|
161
|
+
# Strings, Symbols, or numbers. Returns nil when there is none, or it is of another type, which
|
|
162
|
+
# is then left where it is and sent to Sentry as data. As for the user, taking it from the
|
|
163
|
+
# payload is deprecated.
|
|
164
|
+
def extract_fingerprint!(source, from_payload: false)
|
|
165
|
+
fingerprint = source[:fingerprint]
|
|
166
|
+
return unless fingerprint.is_a?(Array) && fingerprint.all? { |part| fingerprint_part?(part) }
|
|
167
|
+
|
|
168
|
+
payload_context_deprecated if from_payload
|
|
169
|
+
source.delete(:fingerprint).map(&:to_s)
|
|
170
|
+
end
|
|
171
|
+
|
|
172
|
+
def fingerprint_part?(part)
|
|
173
|
+
FINGERPRINT_PART_TYPES.any? { |type| part.is_a?(type) }
|
|
174
|
+
end
|
|
175
|
+
|
|
176
|
+
# Returns [Hash] a copy of the payload that keys can be taken out of. A payload that is not a
|
|
177
|
+
# Hash is sent to Sentry as the extra named payload.
|
|
178
|
+
def payload_copy(payload)
|
|
179
|
+
case payload
|
|
180
|
+
when nil then {}
|
|
181
|
+
when Hash then payload.dup
|
|
182
|
+
else {payload: payload}
|
|
183
|
+
end
|
|
184
|
+
end
|
|
185
|
+
|
|
186
|
+
# Warns once that the Sentry user or fingerprint was taken from the payload.
|
|
187
|
+
def payload_context_deprecated
|
|
188
|
+
return if @payload_context_deprecated
|
|
189
|
+
|
|
190
|
+
@payload_context_deprecated = true
|
|
191
|
+
Kernel.warn(
|
|
192
|
+
"SemanticLogger::Appender::SentryRuby took the Sentry user or fingerprint from the log payload. " \
|
|
193
|
+
"This is deprecated and will be removed in v6, since the payload can hold untrusted data: " \
|
|
194
|
+
"set them with named tags instead, for example SemanticLogger.tagged(user_id: user.id).",
|
|
195
|
+
category: :deprecated
|
|
196
|
+
)
|
|
123
197
|
end
|
|
124
198
|
|
|
125
199
|
# Extract tags.
|
|
@@ -9,6 +9,10 @@ end
|
|
|
9
9
|
#
|
|
10
10
|
# Use the official splunk gem to log messages to Splunk.
|
|
11
11
|
#
|
|
12
|
+
# Deprecated: will be removed in v6. Use the :splunk_http appender instead.
|
|
13
|
+
# The splunk-sdk-ruby gem (last released in 2015) does not verify the server's TLS certificate,
|
|
14
|
+
# so its connection, including the login credentials, is open to interception.
|
|
15
|
+
#
|
|
12
16
|
# Example
|
|
13
17
|
# SemanticLogger.add_appender(
|
|
14
18
|
# appender: :splunk,
|
|
@@ -90,6 +94,11 @@ module SemanticLogger
|
|
|
90
94
|
# Proc: Only include log messages where the supplied Proc returns true
|
|
91
95
|
# The Proc must return true or false.
|
|
92
96
|
def initialize(index: "main", source_type: nil, **args, &)
|
|
97
|
+
Kernel.warn(
|
|
98
|
+
"SemanticLogger::Appender::Splunk (appender: :splunk) is deprecated and will be removed in v6. " \
|
|
99
|
+
"Its splunk-sdk-ruby gem does not verify TLS certificates. Use appender: :splunk_http instead.",
|
|
100
|
+
category: :deprecated
|
|
101
|
+
)
|
|
93
102
|
@index = index
|
|
94
103
|
@source_type = source_type
|
|
95
104
|
|
|
@@ -109,7 +118,7 @@ module SemanticLogger
|
|
|
109
118
|
|
|
110
119
|
# Log the message to Splunk
|
|
111
120
|
def log(log)
|
|
112
|
-
event =
|
|
121
|
+
event = format_log(log)
|
|
113
122
|
service_index.submit(event.delete(:message), event)
|
|
114
123
|
true
|
|
115
124
|
end
|