semantic_logger 5.0.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 +20 -10
- data/Rakefile +40 -0
- data/docs/api.md +506 -0
- data/docs/appenders.md +1074 -0
- data/docs/config.md +707 -0
- data/docs/index.md +223 -0
- data/docs/log.md +116 -0
- data/docs/metrics.md +171 -0
- data/docs/operations.md +446 -0
- data/docs/rails.md +890 -0
- data/docs/security.md +286 -0
- data/docs/testing.md +287 -0
- data/docs/upgrading.md +697 -0
- 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 +126 -29
- data/lib/semantic_logger/appender/elasticsearch.rb +2 -5
- data/lib/semantic_logger/appender/elasticsearch_base.rb +48 -23
- data/lib/semantic_logger/appender/elasticsearch_http.rb +15 -8
- data/lib/semantic_logger/appender/file.rb +141 -28
- 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 +7 -1
- data/lib/semantic_logger/appender/new_relic_logs.rb +11 -12
- data/lib/semantic_logger/appender/open_telemetry.rb +4 -3
- data/lib/semantic_logger/appender/opensearch.rb +2 -1
- data/lib/semantic_logger/appender/rabbitmq.rb +10 -4
- data/lib/semantic_logger/appender/sentry.rb +6 -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 +66 -25
- data/lib/semantic_logger/appender/tcp.rb +10 -4
- data/lib/semantic_logger/appender/udp.rb +11 -1
- data/lib/semantic_logger/appender/wrapper.rb +3 -2
- data/lib/semantic_logger/appender.rb +6 -3
- data/lib/semantic_logger/appenders.rb +11 -2
- data/lib/semantic_logger/base.rb +82 -32
- 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 +22 -3
- data/lib/semantic_logger/formatters/default.rb +28 -6
- data/lib/semantic_logger/formatters/logfmt.rb +155 -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 +11 -5
- data/lib/semantic_logger/formatters/raw.rb +4 -5
- data/lib/semantic_logger/formatters/syslog.rb +110 -17
- data/lib/semantic_logger/formatters/syslog_cee.rb +29 -15
- 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 +12 -8
- 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 +170 -68
- data/lib/semantic_logger/reporters/minitest.rb +4 -4
- 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 +9 -6
- data/lib/semantic_logger/utils.rb +244 -16
- data/lib/semantic_logger/version.rb +1 -1
- data/lib/semantic_logger.rb +1 -1
- metadata +33 -6
- data/lib/semantic_logger/core_ext/thread.rb +0 -30
|
@@ -21,13 +21,14 @@ module SemanticLogger
|
|
|
21
21
|
# Methods forwarded to the queue processor that owns the thread and queue.
|
|
22
22
|
# Tuning options (max_queue_size, batch_size, non_blocking, ...) are set once at
|
|
23
23
|
# construction via SemanticLogger.add_appender, so only the values that callers actually
|
|
24
|
-
# read back are re-exposed here. lag_*
|
|
24
|
+
# read back are re-exposed here. lag_* and non_blocking= back the public
|
|
25
|
+
# SemanticLogger.lag_* and SemanticLogger.non_blocking= API on the main Processor.
|
|
25
26
|
def_delegators :@processor,
|
|
26
27
|
:log, :flush, :close, :thread, :active?, :queue, :max_queue_size,
|
|
27
|
-
:capped?, :non_blocking?, :batch?,
|
|
28
|
+
:capped?, :non_blocking?, :non_blocking=, :batch?,
|
|
28
29
|
:lag_check_interval, :lag_check_interval=,
|
|
29
30
|
:lag_threshold_s, :lag_threshold_s=,
|
|
30
|
-
:processed_count, :dropped_count
|
|
31
|
+
:processed_count, :dropped_count, :failed_count
|
|
31
32
|
|
|
32
33
|
# Appender proxy to allow an existing appender to run asynchronously in a separate thread.
|
|
33
34
|
#
|
|
@@ -73,7 +74,8 @@ module SemanticLogger
|
|
|
73
74
|
#
|
|
74
75
|
# dropped_message_report_seconds: [Integer]
|
|
75
76
|
# When non_blocking is enabled, log the count of dropped messages to the internal logger
|
|
76
|
-
# at most once every this number of seconds.
|
|
77
|
+
# at most once every this number of seconds. Also limits how often messages discarded
|
|
78
|
+
# because their content could not be written are reported, after the first.
|
|
77
79
|
# Default: 30
|
|
78
80
|
#
|
|
79
81
|
# async_max_retries: [Integer]
|
|
@@ -87,6 +89,14 @@ module SemanticLogger
|
|
|
87
89
|
@processor = QueueProcessor.start(appender: appender, **args)
|
|
88
90
|
end
|
|
89
91
|
|
|
92
|
+
# Returns [String] the class and object address of this proxy, whether it batches, and the
|
|
93
|
+
# wrapped appender's own #inspect, which shows only what that appender says is safe, but none of
|
|
94
|
+
# the instance variables of this proxy or its queue processor. See Subscriber#inspect.
|
|
95
|
+
def inspect
|
|
96
|
+
address = Kernel.instance_method(:to_s).bind_call(self).delete_suffix(">")
|
|
97
|
+
"#{address} batch=#{batch?} appender=#{appender.inspect}>"
|
|
98
|
+
end
|
|
99
|
+
|
|
90
100
|
# Re-open appender after a fork.
|
|
91
101
|
def reopen
|
|
92
102
|
appender.reopen if appender.respond_to?(:reopen)
|
|
@@ -104,6 +114,8 @@ module SemanticLogger
|
|
|
104
114
|
# processed: [Integer] Cumulative number of log messages written since startup.
|
|
105
115
|
# dropped: [Integer] Cumulative number of log messages dropped because the queue
|
|
106
116
|
# was full (only possible when non_blocking is enabled).
|
|
117
|
+
# failed: [Integer] Cumulative number of log messages discarded because their
|
|
118
|
+
# content could not be written (see Subscriber#message_error?).
|
|
107
119
|
def stats
|
|
108
120
|
{
|
|
109
121
|
name: name,
|
|
@@ -113,7 +125,8 @@ module SemanticLogger
|
|
|
113
125
|
capped: capped?,
|
|
114
126
|
max_queue_size: capped? ? max_queue_size : nil,
|
|
115
127
|
processed: processed_count,
|
|
116
|
-
dropped: dropped_count
|
|
128
|
+
dropped: dropped_count,
|
|
129
|
+
failed: failed_count
|
|
117
130
|
}
|
|
118
131
|
end
|
|
119
132
|
end
|
|
@@ -2,7 +2,8 @@ begin
|
|
|
2
2
|
require "aws-sdk-cloudwatchlogs"
|
|
3
3
|
rescue LoadError
|
|
4
4
|
raise LoadError,
|
|
5
|
-
|
|
5
|
+
"Gem aws-sdk-cloudwatchlogs is required for logging to CloudWatch Logs. " \
|
|
6
|
+
'Please add the gem "aws-sdk-cloudwatchlogs" to your Gemfile.'
|
|
6
7
|
end
|
|
7
8
|
|
|
8
9
|
require "concurrent"
|
|
@@ -20,7 +21,15 @@ require "concurrent"
|
|
|
20
21
|
module SemanticLogger
|
|
21
22
|
module Appender
|
|
22
23
|
class CloudwatchLogs < SemanticLogger::Subscriber
|
|
23
|
-
|
|
24
|
+
# PutLogEvents limits, from the AWS documentation: a batch holds at most MAX_BATCH_EVENTS
|
|
25
|
+
# events, and at most MAX_BATCH_BYTES counting each event's message in UTF-8 plus
|
|
26
|
+
# EVENT_OVERHEAD_BYTES, in chronological order.
|
|
27
|
+
MAX_BATCH_BYTES = 1_048_576
|
|
28
|
+
MAX_BATCH_EVENTS = 10_000
|
|
29
|
+
EVENT_OVERHEAD_BYTES = 26
|
|
30
|
+
|
|
31
|
+
attr_reader :client_kwargs, :group, :create_group, :create_stream,
|
|
32
|
+
:force_flush_interval_seconds, :max_buffered_events,
|
|
24
33
|
:task, :client, :buffered_logs
|
|
25
34
|
|
|
26
35
|
# Create CloudWatch Logs Appender
|
|
@@ -78,14 +87,19 @@ module SemanticLogger
|
|
|
78
87
|
end
|
|
79
88
|
|
|
80
89
|
# Method called to log an event
|
|
90
|
+
#
|
|
91
|
+
# Formats the log message now, so that one that cannot be formatted, or that CloudWatch Logs
|
|
92
|
+
# would reject, raises here for that message alone, and the buffer only holds log events
|
|
93
|
+
# ready to send.
|
|
81
94
|
def log(log)
|
|
82
|
-
buffered_logs << log
|
|
83
|
-
|
|
95
|
+
buffered_logs << log_event(log)
|
|
84
96
|
put_log_events if buffered_logs.size >= max_buffered_events
|
|
97
|
+
true
|
|
85
98
|
end
|
|
86
99
|
|
|
100
|
+
# Sends the buffered log messages on the calling thread, rather than waiting for the timer.
|
|
87
101
|
def flush
|
|
88
|
-
|
|
102
|
+
put_log_events while buffered_logs.size.positive?
|
|
89
103
|
end
|
|
90
104
|
|
|
91
105
|
def close
|
|
@@ -108,40 +122,123 @@ module SemanticLogger
|
|
|
108
122
|
SemanticLogger::Formatters::Json.new
|
|
109
123
|
end
|
|
110
124
|
|
|
125
|
+
# Only the log group and stream: the client options can hold AWS credentials. See
|
|
126
|
+
# Subscriber#inspect.
|
|
127
|
+
def inspect_details
|
|
128
|
+
"group=#{group.inspect} stream=#{stream.inspect}"
|
|
129
|
+
end
|
|
130
|
+
|
|
111
131
|
private
|
|
112
132
|
|
|
133
|
+
# Returns [Hash] the PutLogEvents log event for the log message.
|
|
134
|
+
def log_event(log)
|
|
135
|
+
{timestamp: (log.time.to_f * 1000).floor, message: event_message(format_log(log))}
|
|
136
|
+
end
|
|
137
|
+
|
|
138
|
+
# Returns [String] the formatted log message in UTF-8, cut short to the most that a batch can
|
|
139
|
+
# hold.
|
|
140
|
+
#
|
|
141
|
+
# Raises FormatError for a message that CloudWatch Logs would reject, together with every
|
|
142
|
+
# other message in the same request: one that is not a String, or is empty.
|
|
143
|
+
def event_message(message)
|
|
144
|
+
unless message.is_a?(String)
|
|
145
|
+
raise(FormatError, "Could not format the log message (the formatter returned a #{message.class})")
|
|
146
|
+
end
|
|
147
|
+
|
|
148
|
+
message = Utils.encode_utf8_string(message)
|
|
149
|
+
raise(FormatError, "Could not format the log message (it is empty)") if message.empty?
|
|
150
|
+
|
|
151
|
+
limit = MAX_BATCH_BYTES - EVENT_OVERHEAD_BYTES
|
|
152
|
+
message.bytesize <= limit ? message : message.byteslice(0, limit).scrub("")
|
|
153
|
+
end
|
|
154
|
+
|
|
155
|
+
# Sends the buffered log events, in as many requests as the PutLogEvents limits need.
|
|
156
|
+
#
|
|
157
|
+
# A request that fails is reported to the internal logger rather than raised: this usually
|
|
158
|
+
# runs on the timer thread, which discards errors, and the log events were already taken off
|
|
159
|
+
# the buffer, so AWS rejecting a batch, for example one over the size limit, lost every
|
|
160
|
+
# message in it without a trace.
|
|
113
161
|
def put_log_events
|
|
114
|
-
|
|
162
|
+
events = buffered_logs.shift(max_buffered_events)
|
|
163
|
+
return if events.none?
|
|
115
164
|
|
|
116
|
-
|
|
165
|
+
batches(events).each { |batch| put_batch(batch) }
|
|
166
|
+
end
|
|
117
167
|
|
|
168
|
+
# Returns [Array<Array<Hash>>] the log events in chronological order, as PutLogEvents requires,
|
|
169
|
+
# split into batches that it accepts.
|
|
170
|
+
def batches(events)
|
|
171
|
+
batches = [[]]
|
|
172
|
+
bytes = 0
|
|
173
|
+
# Keeps the order of log events with the same timestamp.
|
|
174
|
+
events.each_with_index.sort_by { |event, index| [event[:timestamp], index] }.each do |event, _index|
|
|
175
|
+
size = event[:message].bytesize + EVENT_OVERHEAD_BYTES
|
|
176
|
+
if batches.last.size >= MAX_BATCH_EVENTS || bytes + size > MAX_BATCH_BYTES
|
|
177
|
+
batches << []
|
|
178
|
+
bytes = 0
|
|
179
|
+
end
|
|
180
|
+
batches.last << event
|
|
181
|
+
bytes += size
|
|
182
|
+
end
|
|
183
|
+
batches.reject(&:empty?)
|
|
184
|
+
end
|
|
185
|
+
|
|
186
|
+
# Sends one batch, reporting any failure to the internal logger, so that the other batches are
|
|
187
|
+
# still sent.
|
|
188
|
+
def put_batch(events)
|
|
189
|
+
response = send_batch(events)
|
|
190
|
+
report_rejected_events(events, response.rejected_log_events_info)
|
|
191
|
+
rescue StandardError => e
|
|
192
|
+
logger.error("Could not send #{events.size} log message(s) to CloudWatch Logs", e)
|
|
193
|
+
end
|
|
194
|
+
|
|
195
|
+
# Returns the PutLogEvents response, after creating the log group or stream when it is missing
|
|
196
|
+
# and this appender may create it.
|
|
197
|
+
def send_batch(events)
|
|
198
|
+
created = []
|
|
118
199
|
begin
|
|
119
|
-
client.put_log_events(
|
|
120
|
-
log_group_name: group,
|
|
121
|
-
log_stream_name: stream,
|
|
122
|
-
log_events: logs.map do |log|
|
|
123
|
-
{
|
|
124
|
-
timestamp: (log.time.to_f * 1000).floor,
|
|
125
|
-
message: formatter.call(log, self)
|
|
126
|
-
}
|
|
127
|
-
end
|
|
128
|
-
})
|
|
200
|
+
client.put_log_events(log_group_name: group, log_stream_name: stream, log_events: events)
|
|
129
201
|
rescue Aws::CloudWatchLogs::Errors::ResourceNotFoundException => e
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
client.create_log_stream({
|
|
137
|
-
log_group_name: group,
|
|
138
|
-
log_stream_name: stream
|
|
139
|
-
})
|
|
140
|
-
retry
|
|
141
|
-
end
|
|
202
|
+
resource = missing_resource(e)
|
|
203
|
+
raise if resource.nil? || created.include?(resource)
|
|
204
|
+
|
|
205
|
+
create(resource)
|
|
206
|
+
created << resource
|
|
207
|
+
retry
|
|
142
208
|
end
|
|
143
209
|
end
|
|
144
210
|
|
|
211
|
+
# Returns [:group | :stream | nil] the missing resource that this appender may create.
|
|
212
|
+
def missing_resource(exception)
|
|
213
|
+
if exception.message.include?("log group does not exist.") && create_group
|
|
214
|
+
:group
|
|
215
|
+
elsif exception.message.include?("log stream does not exist.") && create_stream
|
|
216
|
+
:stream
|
|
217
|
+
end
|
|
218
|
+
end
|
|
219
|
+
|
|
220
|
+
def create(resource)
|
|
221
|
+
if resource == :group
|
|
222
|
+
client.create_log_group(log_group_name: group)
|
|
223
|
+
else
|
|
224
|
+
client.create_log_stream(log_group_name: group, log_stream_name: stream)
|
|
225
|
+
end
|
|
226
|
+
rescue Aws::CloudWatchLogs::Errors::ResourceAlreadyExistsException
|
|
227
|
+
# Created meanwhile, for example by another process logging to the same log group.
|
|
228
|
+
nil
|
|
229
|
+
end
|
|
230
|
+
|
|
231
|
+
# PutLogEvents accepts a request in which it rejects some of the log events: those more than 2
|
|
232
|
+
# hours in the future, older than 14 days, or older than the log group's retention period.
|
|
233
|
+
def report_rejected_events(events, rejected)
|
|
234
|
+
return unless rejected
|
|
235
|
+
|
|
236
|
+
logger.warn(
|
|
237
|
+
"CloudWatch Logs rejected some of #{events.size} log message(s) as too new, too old, or expired",
|
|
238
|
+
rejected.to_h
|
|
239
|
+
)
|
|
240
|
+
end
|
|
241
|
+
|
|
145
242
|
def stream
|
|
146
243
|
@stream || host
|
|
147
244
|
end
|
|
@@ -2,7 +2,8 @@ begin
|
|
|
2
2
|
require "elasticsearch"
|
|
3
3
|
rescue LoadError
|
|
4
4
|
raise LoadError,
|
|
5
|
-
|
|
5
|
+
"Gem elasticsearch is required for logging to Elasticsearch. " \
|
|
6
|
+
'Please add the gem "elasticsearch" to your Gemfile.'
|
|
6
7
|
end
|
|
7
8
|
|
|
8
9
|
require "semantic_logger/appender/elasticsearch_base"
|
|
@@ -26,10 +27,6 @@ module SemanticLogger
|
|
|
26
27
|
def client_class
|
|
27
28
|
::Elasticsearch::Client
|
|
28
29
|
end
|
|
29
|
-
|
|
30
|
-
def version_supports_type?
|
|
31
|
-
Gem::Version.new(::Elasticsearch::VERSION) < Gem::Version.new(7)
|
|
32
|
-
end
|
|
33
30
|
end
|
|
34
31
|
end
|
|
35
32
|
end
|
|
@@ -6,14 +6,13 @@ module SemanticLogger
|
|
|
6
6
|
#
|
|
7
7
|
# Implements the shared bulk-indexing pipeline used by both the
|
|
8
8
|
# {Elasticsearch} and {OpenSearch} appenders. Subclasses only need to
|
|
9
|
-
# supply the backing client class
|
|
10
|
-
# version still supports document `_type`).
|
|
9
|
+
# supply the backing client class.
|
|
11
10
|
#
|
|
12
11
|
# This class is internal: applications add an appender via
|
|
13
12
|
# `SemanticLogger.add_appender(appender: :elasticsearch, ...)` or
|
|
14
13
|
# `appender: :opensearch`, never by referencing this class directly.
|
|
15
14
|
class ElasticsearchBase < SemanticLogger::Subscriber
|
|
16
|
-
attr_accessor :url, :index, :date_pattern, :
|
|
15
|
+
attr_accessor :url, :index, :date_pattern, :client, :flush_interval, :timeout_interval, :batch_size,
|
|
17
16
|
:client_args
|
|
18
17
|
|
|
19
18
|
# Create an Elasticsearch-compatible appender over persistent HTTP(S).
|
|
@@ -31,9 +30,9 @@ module SemanticLogger
|
|
|
31
30
|
# Default: '%Y.%m.%d'
|
|
32
31
|
#
|
|
33
32
|
# type: [String]
|
|
34
|
-
#
|
|
35
|
-
#
|
|
36
|
-
#
|
|
33
|
+
# Deprecated and ignored. Document `_type` has been unused since
|
|
34
|
+
# Elasticsearch 7 and is ignored by OpenSearch. Passing it logs a
|
|
35
|
+
# deprecation warning; it will be removed in v6.
|
|
37
36
|
#
|
|
38
37
|
# level: [:trace | :debug | :info | :warn | :error | :fatal]
|
|
39
38
|
# Override the log level for this appender.
|
|
@@ -124,7 +123,7 @@ module SemanticLogger
|
|
|
124
123
|
def initialize(url: "http://localhost:9200",
|
|
125
124
|
index: "semantic_logger",
|
|
126
125
|
date_pattern: "%Y.%m.%d",
|
|
127
|
-
type:
|
|
126
|
+
type: nil,
|
|
128
127
|
level: nil,
|
|
129
128
|
formatter: nil,
|
|
130
129
|
filter: nil,
|
|
@@ -134,40 +133,60 @@ module SemanticLogger
|
|
|
134
133
|
data_stream: false,
|
|
135
134
|
**client_args,
|
|
136
135
|
&)
|
|
136
|
+
if type
|
|
137
|
+
Kernel.warn(
|
|
138
|
+
"The Elasticsearch/OpenSearch appender `type:` parameter is deprecated and will be removed in v6. " \
|
|
139
|
+
"Document `_type` has been unused since Elasticsearch 7 and is ignored by OpenSearch.",
|
|
140
|
+
category: :deprecated
|
|
141
|
+
)
|
|
142
|
+
end
|
|
143
|
+
|
|
137
144
|
@url = url
|
|
138
145
|
@index = index
|
|
139
146
|
@date_pattern = date_pattern
|
|
140
|
-
@type = type
|
|
141
147
|
@client_args = client_args.dup
|
|
142
148
|
@client_args[:url] = url if url && !client_args[:hosts]
|
|
143
149
|
@client_args[:logger] = logger
|
|
144
150
|
@data_stream = data_stream
|
|
145
151
|
|
|
146
|
-
super(level: level, formatter: formatter, filter: filter, application: application,
|
|
152
|
+
super(level: level, formatter: formatter, filter: filter, application: application,
|
|
153
|
+
environment: environment, host: host, metrics: false, &)
|
|
147
154
|
reopen
|
|
148
155
|
end
|
|
149
156
|
|
|
157
|
+
# The url is left to the client gem to parse, since it also takes a comma separated list of
|
|
158
|
+
# urls, an Array of them, or hosts without a scheme.
|
|
150
159
|
def reopen
|
|
151
|
-
@client = client_class.new(@client_args)
|
|
160
|
+
@client = Utils.without_url_in_errors(option: ":url or :hosts") { client_class.new(@client_args) }
|
|
152
161
|
end
|
|
153
162
|
|
|
154
163
|
# Log to the index for today
|
|
155
164
|
def log(log)
|
|
156
|
-
|
|
157
|
-
bulk_write([bulk_index(log), bulk_payload])
|
|
165
|
+
bulk_write([bulk_index(log), bulk_line(log)])
|
|
158
166
|
true
|
|
159
167
|
end
|
|
160
168
|
|
|
161
169
|
def batch(logs)
|
|
162
170
|
messages = []
|
|
163
171
|
logs.each do |log|
|
|
164
|
-
messages << bulk_index(log) <<
|
|
172
|
+
messages << bulk_index(log) << bulk_line(log)
|
|
165
173
|
end
|
|
166
174
|
|
|
167
175
|
bulk_write(messages)
|
|
168
176
|
true
|
|
169
177
|
end
|
|
170
178
|
|
|
179
|
+
# The whole batch is formatted into a single bulk request before anything is sent.
|
|
180
|
+
def batch_all_or_nothing?
|
|
181
|
+
true
|
|
182
|
+
end
|
|
183
|
+
|
|
184
|
+
# The url without its credentials, and the index. See Subscriber#inspect.
|
|
185
|
+
def inspect_details
|
|
186
|
+
url_detail = @client_args.key?(:url) ? "url=#{Utils.redacted_url(url).inspect} " : ""
|
|
187
|
+
"#{url_detail}index=#{index.inspect}"
|
|
188
|
+
end
|
|
189
|
+
|
|
171
190
|
private
|
|
172
191
|
|
|
173
192
|
# The backing client class, e.g. ::Elasticsearch::Client or ::OpenSearch::Client.
|
|
@@ -175,12 +194,25 @@ module SemanticLogger
|
|
|
175
194
|
raise NotImplementedError, "#{self.class} must implement #client_class"
|
|
176
195
|
end
|
|
177
196
|
|
|
197
|
+
# Returns [String] the log message formatted as a bulk line, serialized by Utils.to_json
|
|
198
|
+
# and handed to the client as a String. The client gem's serializer writes Infinity and NaN
|
|
199
|
+
# as bare words, which is not JSON, and does not repair invalid UTF-8, a structure nested too
|
|
200
|
+
# deeply, or a Struct (written through its #inspect), so such an entry was dropped or its
|
|
201
|
+
# objects' instance variables were written. Serializing is part of formatting, so an error
|
|
202
|
+
# doing so discards the message rather than stalling the appender (see #format_log). A
|
|
203
|
+
# formatter that returns a String is taken to have serialized the line itself.
|
|
204
|
+
def bulk_line(log)
|
|
205
|
+
format_log(log) { |document| document.is_a?(String) ? document : Utils.to_json(document) }
|
|
206
|
+
end
|
|
207
|
+
|
|
208
|
+
# Serializes the action lines; the documents already are (see #bulk_line).
|
|
178
209
|
def bulk_write(messages)
|
|
210
|
+
body = messages.map { |message| message.is_a?(String) ? message : message.to_json }
|
|
179
211
|
bulk_result =
|
|
180
212
|
if @data_stream
|
|
181
|
-
@client.bulk(index: index, body:
|
|
213
|
+
@client.bulk(index: index, body: body)
|
|
182
214
|
else
|
|
183
|
-
@client.bulk(body:
|
|
215
|
+
@client.bulk(body: body)
|
|
184
216
|
end
|
|
185
217
|
|
|
186
218
|
return unless bulk_result["errors"]
|
|
@@ -193,20 +225,13 @@ module SemanticLogger
|
|
|
193
225
|
expanded_index_name = log.time.strftime("#{index}-#{date_pattern}")
|
|
194
226
|
return {"create" => {}} if @data_stream
|
|
195
227
|
|
|
196
|
-
|
|
197
|
-
bulk_index["index"].merge!({"_type" => type}) if version_supports_type?
|
|
198
|
-
bulk_index
|
|
228
|
+
{"index" => {"_index" => expanded_index_name}}
|
|
199
229
|
end
|
|
200
230
|
|
|
201
231
|
def default_formatter
|
|
202
232
|
time_key = @data_stream ? "@timestamp" : :timestamp
|
|
203
233
|
SemanticLogger::Formatters::Raw.new(time_format: :iso_8601, time_key: time_key)
|
|
204
234
|
end
|
|
205
|
-
|
|
206
|
-
# Modern Elasticsearch (>= 7) and OpenSearch no longer support document `_type`.
|
|
207
|
-
def version_supports_type?
|
|
208
|
-
false
|
|
209
|
-
end
|
|
210
235
|
end
|
|
211
236
|
end
|
|
212
237
|
end
|
|
@@ -13,7 +13,7 @@ require "date"
|
|
|
13
13
|
module SemanticLogger
|
|
14
14
|
module Appender
|
|
15
15
|
class ElasticsearchHttp < SemanticLogger::Appender::Http
|
|
16
|
-
attr_accessor :index
|
|
16
|
+
attr_accessor :index
|
|
17
17
|
|
|
18
18
|
# Create Elasticsearch appender over persistent HTTP(S)
|
|
19
19
|
#
|
|
@@ -25,9 +25,9 @@ module SemanticLogger
|
|
|
25
25
|
# Default: 'semantic_logger'
|
|
26
26
|
#
|
|
27
27
|
# type: [String]
|
|
28
|
-
#
|
|
29
|
-
#
|
|
30
|
-
#
|
|
28
|
+
# Deprecated and ignored. Document `_type` has been unused since
|
|
29
|
+
# Elasticsearch 7. Passing it logs a deprecation warning; it will be
|
|
30
|
+
# removed in v6.
|
|
31
31
|
#
|
|
32
32
|
# level: [:trace | :debug | :info | :warn | :error | :fatal]
|
|
33
33
|
# Override the log level for this appender.
|
|
@@ -52,21 +52,28 @@ module SemanticLogger
|
|
|
52
52
|
# Name of this application to appear in log messages.
|
|
53
53
|
# Default: SemanticLogger.application
|
|
54
54
|
def initialize(index: "semantic_logger",
|
|
55
|
-
type:
|
|
55
|
+
type: nil,
|
|
56
56
|
url: "http://localhost:9200",
|
|
57
57
|
**http_args,
|
|
58
58
|
&)
|
|
59
|
+
if type
|
|
60
|
+
Kernel.warn(
|
|
61
|
+
"The Elasticsearch/OpenSearch appender `type:` parameter is deprecated and will be removed in v6. " \
|
|
62
|
+
"Document `_type` has been unused since Elasticsearch 7 and is ignored by OpenSearch.",
|
|
63
|
+
category: :deprecated
|
|
64
|
+
)
|
|
65
|
+
end
|
|
66
|
+
|
|
59
67
|
@index = index
|
|
60
|
-
@type = type
|
|
61
68
|
super(url: url, **http_args, &)
|
|
62
69
|
|
|
63
70
|
@request_path = "#{@path.end_with?('/') ? @path : "#{@path}/"}#{@index}-%Y.%m.%d"
|
|
64
|
-
@logging_path = "#{@request_path}
|
|
71
|
+
@logging_path = "#{@request_path}/_doc"
|
|
65
72
|
end
|
|
66
73
|
|
|
67
74
|
# Log to the index for today.
|
|
68
75
|
def log(log)
|
|
69
|
-
post(
|
|
76
|
+
post(format_log(log), log.time.strftime(@logging_path))
|
|
70
77
|
end
|
|
71
78
|
|
|
72
79
|
# Deletes all log data captured for a day.
|