semantic_logger 4.18.0 → 5.0.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 +42 -81
- data/Rakefile +10 -3
- data/lib/semantic_logger/appender/async.rb +86 -173
- data/lib/semantic_logger/appender/cloudwatch_logs.rb +4 -4
- data/lib/semantic_logger/appender/elasticsearch.rb +6 -182
- data/lib/semantic_logger/appender/elasticsearch_base.rb +212 -0
- data/lib/semantic_logger/appender/elasticsearch_http.rb +2 -2
- data/lib/semantic_logger/appender/file.rb +20 -4
- data/lib/semantic_logger/appender/graylog.rb +2 -2
- data/lib/semantic_logger/appender/http.rb +27 -2
- data/lib/semantic_logger/appender/io.rb +8 -4
- data/lib/semantic_logger/appender/kafka.rb +2 -2
- data/lib/semantic_logger/appender/loki.rb +2 -4
- data/lib/semantic_logger/appender/mongodb.rb +3 -6
- data/lib/semantic_logger/appender/new_relic_logs.rb +2 -2
- data/lib/semantic_logger/appender/open_telemetry.rb +8 -6
- data/lib/semantic_logger/appender/opensearch.rb +35 -0
- data/lib/semantic_logger/appender/rabbitmq.rb +3 -3
- data/lib/semantic_logger/appender/sentry_ruby.rb +2 -2
- data/lib/semantic_logger/appender/splunk.rb +2 -2
- data/lib/semantic_logger/appender/splunk_http.rb +3 -3
- data/lib/semantic_logger/appender/syslog.rb +5 -4
- data/lib/semantic_logger/appender/tcp.rb +2 -2
- data/lib/semantic_logger/appender/udp.rb +2 -2
- data/lib/semantic_logger/appender/wrapper.rb +4 -4
- data/lib/semantic_logger/appender.rb +30 -19
- data/lib/semantic_logger/appenders.rb +26 -5
- data/lib/semantic_logger/base.rb +100 -21
- data/lib/semantic_logger/concerns/compatibility.rb +2 -2
- data/lib/semantic_logger/core_ext/process.rb +34 -0
- data/lib/semantic_logger/formatters/base.rb +46 -7
- data/lib/semantic_logger/formatters/color.rb +6 -3
- data/lib/semantic_logger/formatters/default.rb +6 -4
- data/lib/semantic_logger/formatters/ecs.rb +151 -0
- data/lib/semantic_logger/formatters/fluentd.rb +15 -4
- data/lib/semantic_logger/formatters/json.rb +6 -1
- data/lib/semantic_logger/formatters/logfmt.rb +2 -2
- data/lib/semantic_logger/formatters/loki.rb +4 -4
- data/lib/semantic_logger/formatters/open_telemetry.rb +15 -5
- data/lib/semantic_logger/formatters/pattern.rb +235 -0
- data/lib/semantic_logger/formatters/raw.rb +2 -2
- data/lib/semantic_logger/formatters/signalfx.rb +2 -2
- data/lib/semantic_logger/formatters/syslog.rb +14 -3
- data/lib/semantic_logger/formatters/syslog_cee.rb +1 -1
- data/lib/semantic_logger/formatters.rb +2 -0
- data/lib/semantic_logger/log.rb +18 -4
- data/lib/semantic_logger/logger.rb +2 -2
- data/lib/semantic_logger/metric/new_relic.rb +2 -2
- data/lib/semantic_logger/metric/signalfx.rb +2 -2
- data/lib/semantic_logger/metric/statsd.rb +2 -2
- data/lib/semantic_logger/processor.rb +21 -0
- data/lib/semantic_logger/queue_processor.rb +369 -0
- data/lib/semantic_logger/reporters/minitest.rb +4 -4
- data/lib/semantic_logger/semantic_logger.rb +103 -11
- data/lib/semantic_logger/subscriber.rb +15 -2
- data/lib/semantic_logger/sync_processor.rb +25 -3
- data/lib/semantic_logger/test/capture_log_events.rb +2 -2
- data/lib/semantic_logger/test/minitest.rb +8 -4
- data/lib/semantic_logger/test/rspec.rb +249 -0
- data/lib/semantic_logger/utils.rb +83 -4
- data/lib/semantic_logger/version.rb +1 -1
- data/lib/semantic_logger.rb +9 -0
- metadata +17 -8
- data/lib/semantic_logger/appender/async_batch.rb +0 -93
|
@@ -29,7 +29,11 @@ module SemanticLogger
|
|
|
29
29
|
formatter: SemanticLogger::Formatters::OpenTelemetry.new,
|
|
30
30
|
metrics: true,
|
|
31
31
|
**args,
|
|
32
|
-
&
|
|
32
|
+
&)
|
|
33
|
+
# Base#initialize (via Subscriber) overwrites @name with the class name,
|
|
34
|
+
# so call super first and then assign our own attributes.
|
|
35
|
+
super(formatter: formatter, metrics: metrics, **args, &)
|
|
36
|
+
|
|
33
37
|
@name = name
|
|
34
38
|
@version = version
|
|
35
39
|
@provider = ::OpenTelemetry.logger_provider
|
|
@@ -38,8 +42,6 @@ module SemanticLogger
|
|
|
38
42
|
# Capture the current Open Telemetry context when a log entry is captured.
|
|
39
43
|
# Prevents duplicate subscribers as long as it is from a constant.
|
|
40
44
|
SemanticLogger.on_log(CAPTURE_CONTEXT)
|
|
41
|
-
|
|
42
|
-
super(formatter: formatter, metrics: metrics, **args, &block)
|
|
43
45
|
end
|
|
44
46
|
|
|
45
47
|
def log(log)
|
|
@@ -57,7 +59,7 @@ module SemanticLogger
|
|
|
57
59
|
timestamp: time,
|
|
58
60
|
body: body.transform_keys!(&:to_s),
|
|
59
61
|
attributes: payload,
|
|
60
|
-
context: log.context[:open_telemetry] || ::OpenTelemetry::Context.current
|
|
62
|
+
context: (log.context && log.context[:open_telemetry]) || ::OpenTelemetry::Context.current
|
|
61
63
|
)
|
|
62
64
|
true
|
|
63
65
|
end
|
|
@@ -69,7 +71,7 @@ module SemanticLogger
|
|
|
69
71
|
@provider.force_flush if @provider.respond_to?(:force_flush)
|
|
70
72
|
rescue StandardError => e
|
|
71
73
|
# Swallow to avoid noisy shutdown exceptions.
|
|
72
|
-
SemanticLogger.logger.warn("Flush failed: #{e.class}: #{e.message}")
|
|
74
|
+
SemanticLogger::Processor.logger.warn("Flush failed: #{e.class}: #{e.message}")
|
|
73
75
|
end
|
|
74
76
|
|
|
75
77
|
# Close the appender and release resources.
|
|
@@ -78,7 +80,7 @@ module SemanticLogger
|
|
|
78
80
|
|
|
79
81
|
@provider.shutdown if @provider.respond_to?(:shutdown)
|
|
80
82
|
rescue StandardError => e
|
|
81
|
-
SemanticLogger.logger.warn("Shutdown failed: #{e.class}: #{e.message}")
|
|
83
|
+
SemanticLogger::Processor.logger.warn("Shutdown failed: #{e.class}: #{e.message}")
|
|
82
84
|
ensure
|
|
83
85
|
@provider = nil
|
|
84
86
|
end
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
begin
|
|
2
|
+
require "opensearch-ruby"
|
|
3
|
+
rescue LoadError
|
|
4
|
+
raise LoadError,
|
|
5
|
+
'Gem opensearch-ruby is required for logging to OpenSearch. Please add the gem "opensearch-ruby" to your Gemfile.'
|
|
6
|
+
end
|
|
7
|
+
|
|
8
|
+
require "semantic_logger/appender/elasticsearch_base"
|
|
9
|
+
|
|
10
|
+
# Forward all log messages to OpenSearch (e.g. AWS OpenSearch).
|
|
11
|
+
#
|
|
12
|
+
# OpenSearch is a fork of Elasticsearch 7.10. The client API and bulk
|
|
13
|
+
# protocol are identical, so this appender reuses the same pipeline as the
|
|
14
|
+
# Elasticsearch appender; only the backing client gem differs. Use this
|
|
15
|
+
# appender (with the `opensearch-ruby` gem) instead of `:elasticsearch`
|
|
16
|
+
# when talking to an OpenSearch server, since modern `elasticsearch` gems
|
|
17
|
+
# reject non-Elasticsearch servers via a product check.
|
|
18
|
+
#
|
|
19
|
+
# Example:
|
|
20
|
+
#
|
|
21
|
+
# SemanticLogger.add_appender(
|
|
22
|
+
# appender: :opensearch,
|
|
23
|
+
# url: 'http://localhost:9200'
|
|
24
|
+
# )
|
|
25
|
+
module SemanticLogger
|
|
26
|
+
module Appender
|
|
27
|
+
class Opensearch < ElasticsearchBase
|
|
28
|
+
private
|
|
29
|
+
|
|
30
|
+
def client_class
|
|
31
|
+
::OpenSearch::Client
|
|
32
|
+
end
|
|
33
|
+
end
|
|
34
|
+
end
|
|
35
|
+
end
|
|
@@ -12,7 +12,7 @@ end
|
|
|
12
12
|
# appender: :rabbitmq,
|
|
13
13
|
#
|
|
14
14
|
# # Name of the queue in RabbitMQ where to publish the logs. This queue will be bound to "amqp.direct" exchange.
|
|
15
|
-
#
|
|
15
|
+
# queue_name: 'semantic_logger',
|
|
16
16
|
#
|
|
17
17
|
# # This host will be used for RabbitMQ connection.
|
|
18
18
|
# # NOTE this is different than :host option which is used by the logger directly.
|
|
@@ -82,13 +82,13 @@ module SemanticLogger
|
|
|
82
82
|
# more parameters supported by Bunny: http://rubybunny.info/articles/connecting.html
|
|
83
83
|
def initialize(queue_name: "semantic_logger", rabbitmq_host: nil,
|
|
84
84
|
level: nil, formatter: nil, filter: nil, application: nil, environment: nil, host: nil, metrics: true,
|
|
85
|
-
**args, &
|
|
85
|
+
**args, &)
|
|
86
86
|
@queue_name = queue_name
|
|
87
87
|
@rabbitmq_args = args.dup
|
|
88
88
|
@rabbitmq_args[:host] = rabbitmq_host
|
|
89
89
|
@rabbitmq_args[:logger] = logger
|
|
90
90
|
|
|
91
|
-
super(level: level, formatter: formatter, filter: filter, application: application, environment: environment, host: host, metrics: metrics, &
|
|
91
|
+
super(level: level, formatter: formatter, filter: filter, application: application, environment: environment, host: host, metrics: metrics, &)
|
|
92
92
|
reopen
|
|
93
93
|
end
|
|
94
94
|
|
|
@@ -133,10 +133,10 @@ module SemanticLogger
|
|
|
133
133
|
#
|
|
134
134
|
def extract_tags!(context)
|
|
135
135
|
named_tags = context.delete(:named_tags) || {}
|
|
136
|
-
named_tags = named_tags.
|
|
136
|
+
named_tags = named_tags.to_h { |k, v| [k.to_s, v.to_s] }
|
|
137
137
|
tags = context.delete(:tags)
|
|
138
138
|
named_tags.merge!("tag" => tags.join(", ")) { |_, v1, v2| "#{v1}, #{v2}" } if tags
|
|
139
|
-
named_tags.
|
|
139
|
+
named_tags.to_h { |k, v| [k[0...32], v[0...256]] }
|
|
140
140
|
end
|
|
141
141
|
end
|
|
142
142
|
end
|
|
@@ -89,11 +89,11 @@ module SemanticLogger
|
|
|
89
89
|
# regular expression. All other messages will be ignored.
|
|
90
90
|
# Proc: Only include log messages where the supplied Proc returns true
|
|
91
91
|
# The Proc must return true or false.
|
|
92
|
-
def initialize(index: "main", source_type: nil, **args, &
|
|
92
|
+
def initialize(index: "main", source_type: nil, **args, &)
|
|
93
93
|
@index = index
|
|
94
94
|
@source_type = source_type
|
|
95
95
|
|
|
96
|
-
super(**args, &
|
|
96
|
+
super(**args, &)
|
|
97
97
|
reopen
|
|
98
98
|
end
|
|
99
99
|
|
|
@@ -73,11 +73,11 @@ module SemanticLogger
|
|
|
73
73
|
index: nil,
|
|
74
74
|
compress: true,
|
|
75
75
|
**args,
|
|
76
|
-
&
|
|
76
|
+
&)
|
|
77
77
|
@source_type = source_type
|
|
78
78
|
@index = index
|
|
79
79
|
|
|
80
|
-
super(compress: compress, **args, &
|
|
80
|
+
super(compress: compress, **args, &)
|
|
81
81
|
|
|
82
82
|
# Put splunk auth token in the header of every HTTP post.
|
|
83
83
|
@header["Authorization"] = "Splunk #{token}"
|
|
@@ -98,7 +98,7 @@ module SemanticLogger
|
|
|
98
98
|
}
|
|
99
99
|
message[:sourcetype] = source_type if source_type
|
|
100
100
|
message[:index] = index if index
|
|
101
|
-
|
|
101
|
+
Utils.to_json(message)
|
|
102
102
|
end
|
|
103
103
|
end
|
|
104
104
|
end
|
|
@@ -130,7 +130,7 @@ module SemanticLogger
|
|
|
130
130
|
options: ::Syslog::LOG_PID | ::Syslog::LOG_CONS,
|
|
131
131
|
tcp_client: {},
|
|
132
132
|
**args,
|
|
133
|
-
&
|
|
133
|
+
&)
|
|
134
134
|
@options = options
|
|
135
135
|
@facility = facility
|
|
136
136
|
@max_size = max_size
|
|
@@ -165,7 +165,7 @@ module SemanticLogger
|
|
|
165
165
|
end
|
|
166
166
|
end
|
|
167
167
|
|
|
168
|
-
super(**args, &
|
|
168
|
+
super(**args, &)
|
|
169
169
|
reopen
|
|
170
170
|
end
|
|
171
171
|
|
|
@@ -213,8 +213,9 @@ module SemanticLogger
|
|
|
213
213
|
# Returns [SemanticLogger::Formatters::Base] default formatter for this Appender depending on the protocal selected
|
|
214
214
|
def default_formatter
|
|
215
215
|
if protocol == :syslog
|
|
216
|
-
# Format is text output without the time
|
|
217
|
-
|
|
216
|
+
# Format is text output without the time.
|
|
217
|
+
# Escape control characters so untrusted log data cannot forge syslog entries.
|
|
218
|
+
SemanticLogger::Formatters::Default.new(time_format: :notime, escape_control_chars: true)
|
|
218
219
|
else
|
|
219
220
|
SemanticLogger::Formatters::Syslog.new(facility: facility, level_map: level_map, max_size: max_size)
|
|
220
221
|
end
|
|
@@ -184,7 +184,7 @@ module SemanticLogger
|
|
|
184
184
|
# )
|
|
185
185
|
def initialize(separator: "\n",
|
|
186
186
|
level: nil, formatter: nil, filter: nil, application: nil, environment: nil, host: nil, metrics: false,
|
|
187
|
-
**tcp_client_args, &
|
|
187
|
+
**tcp_client_args, &)
|
|
188
188
|
@separator = separator
|
|
189
189
|
@tcp_client_args = tcp_client_args
|
|
190
190
|
|
|
@@ -192,7 +192,7 @@ module SemanticLogger
|
|
|
192
192
|
Net::TCPClient.logger = logger
|
|
193
193
|
Net::TCPClient.logger.name = "Net::TCPClient"
|
|
194
194
|
|
|
195
|
-
super(level: level, formatter: formatter, filter: filter, application: application, environment: environment, host: host, metrics: metrics, &
|
|
195
|
+
super(level: level, formatter: formatter, filter: filter, application: application, environment: environment, host: host, metrics: metrics, &)
|
|
196
196
|
reopen
|
|
197
197
|
end
|
|
198
198
|
|
|
@@ -61,11 +61,11 @@ module SemanticLogger
|
|
|
61
61
|
# appender: :udp,
|
|
62
62
|
# server: 'server:3300'
|
|
63
63
|
# )
|
|
64
|
-
def initialize(server:, udp_flags: 0, metrics: true, **args, &
|
|
64
|
+
def initialize(server:, udp_flags: 0, metrics: true, **args, &)
|
|
65
65
|
@server = server
|
|
66
66
|
@udp_flags = udp_flags
|
|
67
67
|
|
|
68
|
-
super(metrics: metrics, **args, &
|
|
68
|
+
super(metrics: metrics, **args, &)
|
|
69
69
|
reopen
|
|
70
70
|
end
|
|
71
71
|
|
|
@@ -38,17 +38,17 @@ module SemanticLogger
|
|
|
38
38
|
# logger.info('Hello World', some: :payload)
|
|
39
39
|
#
|
|
40
40
|
# Install the `rails_semantic_logger` gem to replace the Rails logger with Semantic Logger.
|
|
41
|
-
def initialize(logger:, **args, &
|
|
41
|
+
def initialize(logger:, **args, &)
|
|
42
42
|
@logger = logger
|
|
43
43
|
|
|
44
44
|
# Check if the custom appender responds to all the log levels. For example Ruby ::Logger
|
|
45
|
-
does_not_implement = LEVELS[1
|
|
45
|
+
does_not_implement = LEVELS[1..].find { |i| !@logger.respond_to?(i) }
|
|
46
46
|
if does_not_implement
|
|
47
47
|
raise(ArgumentError,
|
|
48
|
-
"Supplied logger does not implement:#{does_not_implement}. It must implement all of #{LEVELS[1
|
|
48
|
+
"Supplied logger does not implement:#{does_not_implement}. It must implement all of #{LEVELS[1..].inspect}")
|
|
49
49
|
end
|
|
50
50
|
|
|
51
|
-
super(**args, &
|
|
51
|
+
super(**args, &)
|
|
52
52
|
end
|
|
53
53
|
|
|
54
54
|
# Pass log calls to the underlying Rails, log4j or Ruby logger
|
|
@@ -2,10 +2,10 @@ module SemanticLogger
|
|
|
2
2
|
module Appender
|
|
3
3
|
# @formatter:off
|
|
4
4
|
autoload :Async, "semantic_logger/appender/async"
|
|
5
|
-
autoload :AsyncBatch, "semantic_logger/appender/async_batch"
|
|
6
5
|
autoload :Bugsnag, "semantic_logger/appender/bugsnag"
|
|
7
6
|
autoload :CloudwatchLogs, "semantic_logger/appender/cloudwatch_logs"
|
|
8
7
|
autoload :Elasticsearch, "semantic_logger/appender/elasticsearch"
|
|
8
|
+
autoload :ElasticsearchBase, "semantic_logger/appender/elasticsearch_base"
|
|
9
9
|
autoload :ElasticsearchHttp, "semantic_logger/appender/elasticsearch_http"
|
|
10
10
|
autoload :File, "semantic_logger/appender/file"
|
|
11
11
|
autoload :Graylog, "semantic_logger/appender/graylog"
|
|
@@ -18,6 +18,7 @@ module SemanticLogger
|
|
|
18
18
|
autoload :MongoDB, "semantic_logger/appender/mongodb"
|
|
19
19
|
autoload :NewRelic, "semantic_logger/appender/new_relic"
|
|
20
20
|
autoload :NewRelicLogs, "semantic_logger/appender/new_relic_logs"
|
|
21
|
+
autoload :Opensearch, "semantic_logger/appender/opensearch"
|
|
21
22
|
autoload :OpenTelemetry, "semantic_logger/appender/open_telemetry"
|
|
22
23
|
autoload :Rabbitmq, "semantic_logger/appender/rabbitmq"
|
|
23
24
|
autoload :Splunk, "semantic_logger/appender/splunk"
|
|
@@ -34,27 +35,37 @@ module SemanticLogger
|
|
|
34
35
|
def self.factory(async: false, batch: nil,
|
|
35
36
|
max_queue_size: 10_000, lag_check_interval: 1_000, lag_threshold_s: 30,
|
|
36
37
|
batch_size: 300, batch_seconds: 5,
|
|
38
|
+
non_blocking: false, dropped_message_report_seconds: 30,
|
|
39
|
+
async_max_retries: 100,
|
|
37
40
|
**args,
|
|
38
|
-
&
|
|
39
|
-
appender = build(**args, &
|
|
41
|
+
&)
|
|
42
|
+
appender = build(**args, &)
|
|
40
43
|
|
|
41
|
-
# If appender implements #batch, then it should use the batch proxy by default
|
|
42
|
-
|
|
44
|
+
# If appender implements #batch, then it should use the batch proxy by default,
|
|
45
|
+
# unless the appender opts out of batching by default (e.g. the HTTP appender).
|
|
46
|
+
batch = true if batch.nil? && appender.respond_to?(:batch) && appender.batch_by_default?
|
|
43
47
|
|
|
44
48
|
if batch == true
|
|
45
|
-
Appender::
|
|
46
|
-
appender:
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
49
|
+
Appender::Async.new(
|
|
50
|
+
appender: appender,
|
|
51
|
+
batch: true,
|
|
52
|
+
max_queue_size: max_queue_size,
|
|
53
|
+
lag_threshold_s: lag_threshold_s,
|
|
54
|
+
batch_size: batch_size,
|
|
55
|
+
batch_seconds: batch_seconds,
|
|
56
|
+
non_blocking: non_blocking,
|
|
57
|
+
dropped_message_report_seconds: dropped_message_report_seconds,
|
|
58
|
+
async_max_retries: async_max_retries
|
|
51
59
|
)
|
|
52
60
|
elsif async == true
|
|
53
61
|
Appender::Async.new(
|
|
54
|
-
appender:
|
|
55
|
-
max_queue_size:
|
|
56
|
-
lag_check_interval:
|
|
57
|
-
lag_threshold_s:
|
|
62
|
+
appender: appender,
|
|
63
|
+
max_queue_size: max_queue_size,
|
|
64
|
+
lag_check_interval: lag_check_interval,
|
|
65
|
+
lag_threshold_s: lag_threshold_s,
|
|
66
|
+
non_blocking: non_blocking,
|
|
67
|
+
dropped_message_report_seconds: dropped_message_report_seconds,
|
|
68
|
+
async_max_retries: async_max_retries
|
|
58
69
|
)
|
|
59
70
|
else
|
|
60
71
|
appender
|
|
@@ -62,13 +73,13 @@ module SemanticLogger
|
|
|
62
73
|
end
|
|
63
74
|
|
|
64
75
|
# Returns [Subscriber] instance from the supplied options.
|
|
65
|
-
def self.build(io: nil, file_name: nil, appender: nil, metric: nil, logger: nil, **args, &
|
|
76
|
+
def self.build(io: nil, file_name: nil, appender: nil, metric: nil, logger: nil, **args, &)
|
|
66
77
|
if file_name
|
|
67
|
-
SemanticLogger::Appender::File.new(file_name, **args, &
|
|
78
|
+
SemanticLogger::Appender::File.new(file_name, **args, &)
|
|
68
79
|
elsif io
|
|
69
|
-
SemanticLogger::Appender::IO.new(io, **args, &
|
|
80
|
+
SemanticLogger::Appender::IO.new(io, **args, &)
|
|
70
81
|
elsif logger
|
|
71
|
-
SemanticLogger::Appender::Wrapper.new(logger: logger, **args, &
|
|
82
|
+
SemanticLogger::Appender::Wrapper.new(logger: logger, **args, &)
|
|
72
83
|
elsif appender
|
|
73
84
|
if appender.is_a?(Symbol)
|
|
74
85
|
SemanticLogger::Utils.constantize_symbol(appender).new(**args)
|
|
@@ -9,11 +9,12 @@ module SemanticLogger
|
|
|
9
9
|
super()
|
|
10
10
|
end
|
|
11
11
|
|
|
12
|
-
def add(**args, &
|
|
13
|
-
appender = SemanticLogger::Appender.factory(**args, &
|
|
12
|
+
def add(**args, &)
|
|
13
|
+
appender = SemanticLogger::Appender.factory(**args, &)
|
|
14
14
|
|
|
15
|
-
|
|
16
|
-
|
|
15
|
+
stream = appender.respond_to?(:console_stream) && appender.console_stream
|
|
16
|
+
if stream && console_streams.include?(stream)
|
|
17
|
+
logger.warn "Ignoring attempt to add a second #{stream} console appender since it would result in duplicate console output."
|
|
17
18
|
return
|
|
18
19
|
end
|
|
19
20
|
|
|
@@ -21,10 +22,30 @@ module SemanticLogger
|
|
|
21
22
|
appender
|
|
22
23
|
end
|
|
23
24
|
|
|
25
|
+
# The console streams (:stdout and/or :stderr) already being written to by the existing appenders.
|
|
26
|
+
def console_streams
|
|
27
|
+
filter_map { |appender| appender.console_stream if appender.respond_to?(:console_stream) }
|
|
28
|
+
end
|
|
29
|
+
|
|
24
30
|
# Whether any of the existing appenders already output to the console?
|
|
25
31
|
# I.e. Writes to stdout or stderr.
|
|
26
32
|
def console_output?
|
|
27
|
-
any?
|
|
33
|
+
console_streams.any?
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
# Returns [Array<Hash>] operational statistics for each appender.
|
|
37
|
+
#
|
|
38
|
+
# Appenders that run asynchronously (see SemanticLogger::Appender::Async#stats) report
|
|
39
|
+
# their queue size and processed/dropped counts. Appenders that log inline on the
|
|
40
|
+
# processor thread report only their name with `async: false`.
|
|
41
|
+
def stats
|
|
42
|
+
map do |appender|
|
|
43
|
+
if appender.respond_to?(:stats)
|
|
44
|
+
appender.stats
|
|
45
|
+
else
|
|
46
|
+
{name: appender.name, async: false}
|
|
47
|
+
end
|
|
48
|
+
end
|
|
28
49
|
end
|
|
29
50
|
|
|
30
51
|
def log(log)
|
data/lib/semantic_logger/base.rb
CHANGED
|
@@ -198,7 +198,18 @@ module SemanticLogger
|
|
|
198
198
|
# to:
|
|
199
199
|
# `logger.tagged('first', 'more', 'other')`
|
|
200
200
|
# - For better performance with clean tags, see `SemanticLogger.tagged`.
|
|
201
|
+
#
|
|
202
|
+
# Child logger:
|
|
203
|
+
# - When called *without* a block, returns a new logger instance that permanently
|
|
204
|
+
# carries the supplied tags ("instance tags"). Those tags are added to every log
|
|
205
|
+
# entry emitted by the returned logger, and _only_ that logger, even across threads.
|
|
206
|
+
# Unlike the block form they are not pushed onto the thread, so other loggers are
|
|
207
|
+
# unaffected. This is the recommended way to bind a logger to an object's identity:
|
|
208
|
+
# logger = SemanticLogger['Cart'].tagged(cart_id: cart.id)
|
|
201
209
|
def tagged(*tags)
|
|
210
|
+
# No block: build a child logger that carries these tags on every entry.
|
|
211
|
+
return tagged_child(tags) unless block_given?
|
|
212
|
+
|
|
202
213
|
block = -> { yield(self) }
|
|
203
214
|
# Allow named tags to be passed into the logger
|
|
204
215
|
# Rails::Rack::Logger passes logs as an array with a single argument
|
|
@@ -226,6 +237,10 @@ module SemanticLogger
|
|
|
226
237
|
SemanticLogger.named_tags
|
|
227
238
|
end
|
|
228
239
|
|
|
240
|
+
# Tags permanently bound to this logger instance via `#tagged` (without a block).
|
|
241
|
+
# Empty for a regular (non-child) logger.
|
|
242
|
+
attr_reader :instance_tags, :instance_named_tags
|
|
243
|
+
|
|
229
244
|
# Returns the list of tags pushed after flattening them out and removing blanks
|
|
230
245
|
#
|
|
231
246
|
# Note:
|
|
@@ -244,13 +259,13 @@ module SemanticLogger
|
|
|
244
259
|
end
|
|
245
260
|
|
|
246
261
|
# :nodoc:
|
|
247
|
-
def silence(new_level = :error, &
|
|
248
|
-
SemanticLogger.silence(new_level, &
|
|
262
|
+
def silence(new_level = :error, &)
|
|
263
|
+
SemanticLogger.silence(new_level, &)
|
|
249
264
|
end
|
|
250
265
|
|
|
251
266
|
# :nodoc:
|
|
252
|
-
def fast_tag(tag, &
|
|
253
|
-
SemanticLogger.fast_tag(tag, &
|
|
267
|
+
def fast_tag(tag, &)
|
|
268
|
+
SemanticLogger.fast_tag(tag, &)
|
|
254
269
|
end
|
|
255
270
|
|
|
256
271
|
# Write log data to underlying data storage
|
|
@@ -263,6 +278,11 @@ module SemanticLogger
|
|
|
263
278
|
meets_log_level?(log) && !filtered?(log)
|
|
264
279
|
end
|
|
265
280
|
|
|
281
|
+
protected
|
|
282
|
+
|
|
283
|
+
# Only assigned to when building a child logger via `#tagged` (see #tagged_child).
|
|
284
|
+
attr_writer :instance_tags, :instance_named_tags
|
|
285
|
+
|
|
266
286
|
private
|
|
267
287
|
|
|
268
288
|
# Initializer for Abstract Class SemanticLogger::Base
|
|
@@ -294,8 +314,13 @@ module SemanticLogger
|
|
|
294
314
|
raise ":filter must be a Regexp, Proc, or implement :call"
|
|
295
315
|
end
|
|
296
316
|
|
|
297
|
-
@filter
|
|
298
|
-
@name
|
|
317
|
+
@filter = filter.is_a?(Regexp) ? filter.freeze : filter
|
|
318
|
+
@name = klass.is_a?(String) ? klass : klass.name
|
|
319
|
+
# `[].freeze` / `{}.freeze` compile to `opt_ary_freeze` / `opt_hash_freeze`, which return
|
|
320
|
+
# a shared frozen singleton. Every logger points at the same two objects, so these default
|
|
321
|
+
# (empty) instance tags allocate no garbage. Child loggers replace them via #tagged_child.
|
|
322
|
+
@instance_tags = [].freeze
|
|
323
|
+
@instance_named_tags = {}.freeze
|
|
299
324
|
if level.nil?
|
|
300
325
|
# Allow the global default level to determine this loggers log level
|
|
301
326
|
@level_index = nil
|
|
@@ -324,13 +349,59 @@ module SemanticLogger
|
|
|
324
349
|
(level_index <= (log.level_index || 0))
|
|
325
350
|
end
|
|
326
351
|
|
|
352
|
+
# Build a new Log entry, layering this logger's instance tags on top of the
|
|
353
|
+
# current thread context.
|
|
354
|
+
def new_log(level, index = nil)
|
|
355
|
+
apply_instance_tags(Log.new(name, level, index))
|
|
356
|
+
end
|
|
357
|
+
|
|
358
|
+
# Merge this logger's instance tags into the supplied Log entry.
|
|
359
|
+
#
|
|
360
|
+
# Composition (mirrors the per-logger merge rule proposed in PR #301):
|
|
361
|
+
# - Positional: thread tags first, then this logger's instance tags appended.
|
|
362
|
+
# - Named: instance named tags merged on top of the thread named tags, so on a
|
|
363
|
+
# key conflict the logger's own identity wins (it is the more specific value).
|
|
364
|
+
# Only this logger's entries are affected; the thread context is left untouched.
|
|
365
|
+
def apply_instance_tags(log)
|
|
366
|
+
unless @instance_tags.empty?
|
|
367
|
+
log.tags = log.tags.empty? ? @instance_tags.dup : log.tags + @instance_tags
|
|
368
|
+
end
|
|
369
|
+
unless @instance_named_tags.empty?
|
|
370
|
+
log.named_tags = log.named_tags.empty? ? @instance_named_tags.dup : log.named_tags.merge(@instance_named_tags)
|
|
371
|
+
end
|
|
372
|
+
log
|
|
373
|
+
end
|
|
374
|
+
|
|
375
|
+
# Returns a copy of this logger that permanently carries the supplied tags.
|
|
376
|
+
# Positional (string) and named (Hash) tags may be mixed; they are merged on
|
|
377
|
+
# top of any instance tags this logger already carries.
|
|
378
|
+
def tagged_child(tags)
|
|
379
|
+
positional = @instance_tags.dup
|
|
380
|
+
named = @instance_named_tags.dup
|
|
381
|
+
tags.flatten.each do |tag|
|
|
382
|
+
case tag
|
|
383
|
+
when Hash
|
|
384
|
+
named = named.merge(tag)
|
|
385
|
+
when nil, ""
|
|
386
|
+
# Ignore blanks for Rails compatibility, matching the block form.
|
|
387
|
+
else
|
|
388
|
+
positional << tag.to_s
|
|
389
|
+
end
|
|
390
|
+
end
|
|
391
|
+
|
|
392
|
+
child = clone
|
|
393
|
+
child.instance_tags = positional.freeze
|
|
394
|
+
child.instance_named_tags = named.freeze
|
|
395
|
+
child
|
|
396
|
+
end
|
|
397
|
+
|
|
327
398
|
# Log message at the specified level
|
|
328
|
-
def log_internal(level, index, message = nil, payload = nil, exception = nil)
|
|
399
|
+
def log_internal(level, index, message = nil, payload = nil, exception = nil, &block)
|
|
329
400
|
# Handle variable number of arguments by detecting exception object and payload hash.
|
|
330
401
|
if exception.nil? && payload.nil? && message.respond_to?(:backtrace) && message.respond_to?(:message)
|
|
331
402
|
exception = message
|
|
332
403
|
message = nil
|
|
333
|
-
elsif exception.nil? && payload
|
|
404
|
+
elsif exception.nil? && payload.respond_to?(:backtrace) && payload.respond_to?(:message)
|
|
334
405
|
exception = payload
|
|
335
406
|
payload = nil
|
|
336
407
|
elsif payload && !payload.is_a?(Hash)
|
|
@@ -338,12 +409,12 @@ module SemanticLogger
|
|
|
338
409
|
payload = nil
|
|
339
410
|
end
|
|
340
411
|
|
|
341
|
-
log =
|
|
412
|
+
log = new_log(level, index)
|
|
342
413
|
should_log =
|
|
343
414
|
if exception.nil? && payload.nil? && message.is_a?(Hash)
|
|
344
415
|
# All arguments as a hash in the message.
|
|
345
416
|
log.assign(**log.extract_arguments(message))
|
|
346
|
-
elsif exception.nil? && message && payload
|
|
417
|
+
elsif exception.nil? && message && payload.is_a?(Hash)
|
|
347
418
|
# Message supplied along with a hash with the remaining arguments.
|
|
348
419
|
log.assign(**log.extract_arguments(payload, message))
|
|
349
420
|
else
|
|
@@ -352,20 +423,28 @@ module SemanticLogger
|
|
|
352
423
|
end
|
|
353
424
|
|
|
354
425
|
# Add result of block to message or payload if not nil
|
|
355
|
-
if
|
|
356
|
-
result = yield(log)
|
|
357
|
-
case result
|
|
358
|
-
when String
|
|
359
|
-
log.message = log.message.nil? ? result : "#{log.message} -- #{result}"
|
|
360
|
-
when Hash
|
|
361
|
-
log.assign_hash(result)
|
|
362
|
-
end
|
|
363
|
-
end
|
|
426
|
+
apply_block_result(log, &block) if block
|
|
364
427
|
|
|
365
428
|
# Log level may change during assign due to :on_exception_level
|
|
366
429
|
self.log(log) if should_log && should_log?(log)
|
|
367
430
|
end
|
|
368
431
|
|
|
432
|
+
# Merge the result of a logging block into the log entry.
|
|
433
|
+
#
|
|
434
|
+
# Zero-arity blocks (e.g. `logger.info { "msg" }` or a zero-arity lambda) are called without
|
|
435
|
+
# arguments. Lambdas enforce their arity, so yielding the log to a zero-arity lambda would
|
|
436
|
+
# raise ArgumentError. Blocks that accept an argument still receive the log so they can read
|
|
437
|
+
# or mutate it.
|
|
438
|
+
def apply_block_result(log, &block)
|
|
439
|
+
result = block.arity.zero? ? block.call : block.call(log)
|
|
440
|
+
case result
|
|
441
|
+
when String
|
|
442
|
+
log.message = log.message.nil? ? result : "#{log.message} -- #{result}"
|
|
443
|
+
when Hash
|
|
444
|
+
log.assign_hash(result)
|
|
445
|
+
end
|
|
446
|
+
end
|
|
447
|
+
|
|
369
448
|
# Measure the supplied block and log the message
|
|
370
449
|
def measure_internal(level, index, message, params)
|
|
371
450
|
exception = nil
|
|
@@ -389,7 +468,7 @@ module SemanticLogger
|
|
|
389
468
|
exception = e
|
|
390
469
|
ensure
|
|
391
470
|
# Must use ensure block otherwise a `return` in the yield above will skip the log entry
|
|
392
|
-
log =
|
|
471
|
+
log = new_log(level, index)
|
|
393
472
|
exception ||= params[:exception]
|
|
394
473
|
message = params[:message] if params[:message]
|
|
395
474
|
duration =
|
|
@@ -437,7 +516,7 @@ module SemanticLogger
|
|
|
437
516
|
rescue Exception => e
|
|
438
517
|
exception = e
|
|
439
518
|
ensure
|
|
440
|
-
log =
|
|
519
|
+
log = new_log(level, index)
|
|
441
520
|
# May return false due to elastic logging
|
|
442
521
|
should_log = log.assign(
|
|
443
522
|
message: message,
|
|
@@ -37,11 +37,11 @@ module SemanticLogger
|
|
|
37
37
|
end
|
|
38
38
|
|
|
39
39
|
# :nodoc:
|
|
40
|
-
def add(severity, message = nil, progname = nil, &
|
|
40
|
+
def add(severity, message = nil, progname = nil, &)
|
|
41
41
|
index = Levels.index(severity)
|
|
42
42
|
if level_index <= index
|
|
43
43
|
level = Levels.level(index)
|
|
44
|
-
log_internal(level, index, message, progname, &
|
|
44
|
+
log_internal(level, index, message, progname, &)
|
|
45
45
|
true
|
|
46
46
|
else
|
|
47
47
|
false
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
module SemanticLogger
|
|
2
|
+
module CoreExt
|
|
3
|
+
# Reopen all appenders in the child process after a fork.
|
|
4
|
+
#
|
|
5
|
+
# Prepended onto `Process.singleton_class` when Semantic Logger is loaded.
|
|
6
|
+
# Enabled by default; opt out with `SemanticLogger.reopen_on_fork = false`.
|
|
7
|
+
#
|
|
8
|
+
# `Process._fork` (Ruby 3.1+) is the single method that every fork path routes
|
|
9
|
+
# through (`Kernel#fork`, `Process.fork`, `IO.popen`, `Kernel#system`, and
|
|
10
|
+
# backticks), so overriding it covers them all with one hook.
|
|
11
|
+
#
|
|
12
|
+
# Note: both `_fork` and `daemon` must be overridden to reliably restart the
|
|
13
|
+
# appender thread, since `Process.daemon` does not route through `_fork`
|
|
14
|
+
# (https://bugs.ruby-lang.org/issues/18911). Do not collapse this down to only
|
|
15
|
+
# `_fork`.
|
|
16
|
+
module Process
|
|
17
|
+
# `_fork` runs in both the parent and the child. It returns 0 in the child
|
|
18
|
+
# and the child's pid in the parent, so only reopen in the child. Reopening
|
|
19
|
+
# in the parent would needlessly recreate its queue and worker thread,
|
|
20
|
+
# risking the loss of messages still on the queue.
|
|
21
|
+
def _fork
|
|
22
|
+
child_pid = super
|
|
23
|
+
SemanticLogger.reopen if child_pid.zero? && SemanticLogger.reopen_on_fork?
|
|
24
|
+
child_pid
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
# `Process.daemon` does not route through `_fork`, so reopen explicitly. Once
|
|
28
|
+
# it returns, the caller is running as the daemon (child) process.
|
|
29
|
+
def daemon(...)
|
|
30
|
+
super.tap { SemanticLogger.reopen if SemanticLogger.reopen_on_fork? }
|
|
31
|
+
end
|
|
32
|
+
end
|
|
33
|
+
end
|
|
34
|
+
end
|