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
|
@@ -48,7 +48,8 @@ module SemanticLogger
|
|
|
48
48
|
def initialize(formatter: SemanticLogger::Formatters::NewRelicLogs.new, **args, &block)
|
|
49
49
|
super
|
|
50
50
|
|
|
51
|
-
# Record NewRelic's "trace.id"/"entity.name"/"hostname"/etc, so we can include them
|
|
51
|
+
# Record NewRelic's "trace.id"/"entity.name"/"hostname"/etc, so we can include them
|
|
52
|
+
# later in the formatted output.
|
|
52
53
|
# These are thread-local, so need to be captured as soon as the log-message is created.
|
|
53
54
|
# https://rubydoc.info/gems/newrelic_rpm/NewRelic/Agent#linking_metadata-instance_method
|
|
54
55
|
SemanticLogger.on_log(CAPTURE_CONTEXT)
|
|
@@ -56,17 +57,15 @@ module SemanticLogger
|
|
|
56
57
|
|
|
57
58
|
# Send an error notification to New Relic
|
|
58
59
|
def log(log)
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
warn("Unexpected error while logging to New Relic: #{e.message}")
|
|
69
|
-
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)
|
|
70
69
|
true
|
|
71
70
|
end
|
|
72
71
|
|
|
@@ -2,7 +2,8 @@ begin
|
|
|
2
2
|
require "opentelemetry/logs"
|
|
3
3
|
rescue LoadError
|
|
4
4
|
raise LoadError,
|
|
5
|
-
|
|
5
|
+
"Gem opentelemetry-logs-sdk is required for logging to Open Telemetry. " \
|
|
6
|
+
'Please add the gem "opentelemetry-logs-sdk" to your Gemfile.'
|
|
6
7
|
end
|
|
7
8
|
|
|
8
9
|
# Open Telemetry Appender
|
|
@@ -47,7 +48,7 @@ module SemanticLogger
|
|
|
47
48
|
def log(log)
|
|
48
49
|
# return log_metric(log) if metrics && log.metric_only?
|
|
49
50
|
|
|
50
|
-
body =
|
|
51
|
+
body = format_log(log)
|
|
51
52
|
level = body.delete(:level)
|
|
52
53
|
level_index = body.delete(:level_index)
|
|
53
54
|
time = body.delete(:time)
|
|
@@ -88,7 +89,7 @@ module SemanticLogger
|
|
|
88
89
|
# For logging metrics only log events.
|
|
89
90
|
# def log_metric(log)
|
|
90
91
|
# puts "**** TODO: Metric Only Event ****"
|
|
91
|
-
# ap
|
|
92
|
+
# ap format_log(log)
|
|
92
93
|
# ap log.payload
|
|
93
94
|
# true
|
|
94
95
|
# end
|
|
@@ -2,7 +2,8 @@ begin
|
|
|
2
2
|
require "opensearch-ruby"
|
|
3
3
|
rescue LoadError
|
|
4
4
|
raise LoadError,
|
|
5
|
-
|
|
5
|
+
"Gem opensearch-ruby is required for logging to OpenSearch. " \
|
|
6
|
+
'Please add the gem "opensearch-ruby" to your Gemfile.'
|
|
6
7
|
end
|
|
7
8
|
|
|
8
9
|
require "semantic_logger/appender/elasticsearch_base"
|
|
@@ -81,14 +81,15 @@ module SemanticLogger
|
|
|
81
81
|
#
|
|
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
|
-
level: nil, formatter: nil, filter: nil, application: nil, environment: nil,
|
|
85
|
-
**args, &)
|
|
84
|
+
level: nil, formatter: nil, filter: nil, application: nil, environment: nil,
|
|
85
|
+
host: nil, metrics: true, **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,
|
|
91
|
+
super(level: level, formatter: formatter, filter: filter, application: application,
|
|
92
|
+
environment: environment, host: host, metrics: metrics, &)
|
|
92
93
|
reopen
|
|
93
94
|
end
|
|
94
95
|
|
|
@@ -106,7 +107,7 @@ module SemanticLogger
|
|
|
106
107
|
end
|
|
107
108
|
|
|
108
109
|
def log(log)
|
|
109
|
-
queue.publish(
|
|
110
|
+
queue.publish(format_log(log))
|
|
110
111
|
end
|
|
111
112
|
|
|
112
113
|
def flush
|
|
@@ -121,6 +122,11 @@ module SemanticLogger
|
|
|
121
122
|
def queue
|
|
122
123
|
@queue ||= @channel.queue(@queue_name)
|
|
123
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
|
|
124
130
|
end
|
|
125
131
|
end
|
|
126
132
|
end
|
|
@@ -39,6 +39,11 @@ module SemanticLogger
|
|
|
39
39
|
# Name of this application to appear in log messages.
|
|
40
40
|
# Default: SemanticLogger.application
|
|
41
41
|
def initialize(level: :error, **args, &block)
|
|
42
|
+
Kernel.warn(
|
|
43
|
+
"SemanticLogger::Appender::Sentry (appender: :sentry) is deprecated and will be removed in v6. " \
|
|
44
|
+
"It depends on the end-of-life sentry-raven gem. Use appender: :sentry_ruby instead.",
|
|
45
|
+
category: :deprecated
|
|
46
|
+
)
|
|
42
47
|
# Replace the Sentry Raven logger so that we can identify its log messages and not forward them to Sentry
|
|
43
48
|
Raven.configure { |config| config.logger = SemanticLogger[Raven] }
|
|
44
49
|
super
|
|
@@ -49,7 +54,7 @@ module SemanticLogger
|
|
|
49
54
|
# Ignore logs coming from Raven itself
|
|
50
55
|
return false if log.name == "Raven"
|
|
51
56
|
|
|
52
|
-
context =
|
|
57
|
+
context = format_log(log)
|
|
53
58
|
user = context.delete(:user)
|
|
54
59
|
tags = context.delete(:tags)
|
|
55
60
|
attrs = {
|
|
@@ -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
|
|
@@ -1,4 +1,3 @@
|
|
|
1
|
-
require "syslog"
|
|
2
1
|
require "uri"
|
|
3
2
|
require "socket"
|
|
4
3
|
# Send log messages to local syslog, or remote syslog servers over TCP or UDP.
|
|
@@ -78,7 +77,7 @@ module SemanticLogger
|
|
|
78
77
|
# Default: 1024 bytes
|
|
79
78
|
#
|
|
80
79
|
# options: [Integer]
|
|
81
|
-
# Default: ::Syslog::LOG_PID | ::Syslog::LOG_CONS
|
|
80
|
+
# Default: ::Syslog::LOG_PID | ::Syslog::LOG_CONS (when logging to a local syslog daemon)
|
|
82
81
|
# Any of the following (options can be logically OR'd together)
|
|
83
82
|
# ::Syslog::LOG_CONS
|
|
84
83
|
# ::Syslog::LOG_NDELAY
|
|
@@ -91,7 +90,7 @@ module SemanticLogger
|
|
|
91
90
|
# I.e. When `url: 'syslog://localhost'`
|
|
92
91
|
#
|
|
93
92
|
# facility: [Integer]
|
|
94
|
-
# Default: ::Syslog::LOG_USER
|
|
93
|
+
# Default: ::Syslog::LOG_USER (the same value as SemanticLogger::Formatters::Syslog::LOG_USER)
|
|
95
94
|
# Type of program (can be logically OR'd together)
|
|
96
95
|
# ::Syslog::LOG_AUTH
|
|
97
96
|
# ::Syslog::LOG_AUTHPRIV
|
|
@@ -124,19 +123,19 @@ module SemanticLogger
|
|
|
124
123
|
# # Change the warn level to LOG_NOTICE level instead of a the default of LOG_WARNING.
|
|
125
124
|
# SemanticLogger.add_appender(appender: :syslog, level_map: {warn: ::Syslog::LOG_NOTICE})
|
|
126
125
|
def initialize(url: "syslog://localhost",
|
|
127
|
-
facility: ::Syslog::LOG_USER,
|
|
128
|
-
max_size:
|
|
126
|
+
facility: SemanticLogger::Formatters::Syslog::LOG_USER,
|
|
127
|
+
max_size: SemanticLogger::Formatters::Syslog::DEFAULT_MAX_SIZE,
|
|
129
128
|
level_map: SemanticLogger::Formatters::Syslog::LevelMap.new,
|
|
130
|
-
options:
|
|
129
|
+
options: nil,
|
|
131
130
|
tcp_client: {},
|
|
132
131
|
**args,
|
|
133
132
|
&)
|
|
134
133
|
@options = options
|
|
135
134
|
@facility = facility
|
|
136
135
|
@max_size = max_size
|
|
137
|
-
@level_map = level_map
|
|
136
|
+
@level_map = SemanticLogger::Formatters::Syslog::LevelMap.build(level_map)
|
|
138
137
|
@url = url
|
|
139
|
-
uri =
|
|
138
|
+
uri = Utils.parse_url(@url)
|
|
140
139
|
@server = uri.host || "localhost"
|
|
141
140
|
@protocol = (uri.scheme || :syslog).to_sym
|
|
142
141
|
@port = uri.port || 514
|
|
@@ -145,26 +144,36 @@ module SemanticLogger
|
|
|
145
144
|
|
|
146
145
|
raise "Unknown protocol #{@protocol}!" unless %i[syslog tcp udp].include?(@protocol)
|
|
147
146
|
|
|
148
|
-
# The
|
|
149
|
-
if
|
|
147
|
+
# The syslog gem, a bundled gem since Ruby 3.4, is only required when logging to a local syslog daemon.
|
|
148
|
+
if @protocol == :syslog
|
|
150
149
|
begin
|
|
151
|
-
require "
|
|
150
|
+
require "syslog"
|
|
152
151
|
rescue LoadError
|
|
153
152
|
raise LoadError,
|
|
154
|
-
"Missing gem:
|
|
153
|
+
"Missing gem: syslog. This gem is required when logging to a local syslog daemon on Ruby 3.4 " \
|
|
154
|
+
'and later. Please add the gem "syslog" to your Gemfile.'
|
|
155
155
|
end
|
|
156
|
+
@options ||= ::Syslog::LOG_PID | ::Syslog::LOG_CONS
|
|
157
|
+
end
|
|
156
158
|
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
159
|
+
# The net_tcp_client gem is required when logging over TCP.
|
|
160
|
+
# The syslog formatters load the syslog_protocol gem that they use to build remote syslog packets.
|
|
161
|
+
if @protocol == :tcp
|
|
162
|
+
begin
|
|
163
|
+
require "net/tcp_client"
|
|
164
|
+
rescue LoadError
|
|
165
|
+
raise LoadError,
|
|
166
|
+
"Missing gem: net_tcp_client. This gem is required when logging over TCP. " \
|
|
167
|
+
"To fix this error: gem install net_tcp_client"
|
|
165
168
|
end
|
|
166
169
|
end
|
|
167
170
|
|
|
171
|
+
if %i[tcp udp].include?(@protocol)
|
|
172
|
+
# Reject an invalid facility when the appender is created, even with a formatter that ignores it.
|
|
173
|
+
SemanticLogger::Formatters::Syslog.facility_number(@facility)
|
|
174
|
+
args[:formatter] = with_packet_options(args[:formatter]) if args.key?(:formatter)
|
|
175
|
+
end
|
|
176
|
+
|
|
168
177
|
super(**args, &)
|
|
169
178
|
reopen
|
|
170
179
|
end
|
|
@@ -193,12 +202,12 @@ module SemanticLogger
|
|
|
193
202
|
case @protocol
|
|
194
203
|
when :syslog
|
|
195
204
|
# Since the Ruby Syslog API supports sprintf format strings, double up all existing '%'
|
|
196
|
-
message =
|
|
205
|
+
message = format_log(log).gsub "%", "%%"
|
|
197
206
|
::Syslog.log @level_map[log.level], message
|
|
198
207
|
when :tcp
|
|
199
|
-
@remote_syslog.retry_on_connection_failure { @remote_syslog.write("#{
|
|
208
|
+
@remote_syslog.retry_on_connection_failure { @remote_syslog.write("#{format_log(log)}\r\n") }
|
|
200
209
|
when :udp
|
|
201
|
-
@remote_syslog.send(
|
|
210
|
+
@remote_syslog.send(format_log(log), 0, @server, @port)
|
|
202
211
|
else
|
|
203
212
|
raise "Unsupported protocol: #{protocol}"
|
|
204
213
|
end
|
|
@@ -210,16 +219,48 @@ module SemanticLogger
|
|
|
210
219
|
@remote_syslog.flush if @remote_syslog.respond_to?(:flush)
|
|
211
220
|
end
|
|
212
221
|
|
|
213
|
-
#
|
|
222
|
+
# Over UDP, a log message too large for a datagram (Errno::EMSGSIZE) cannot be sent by retrying.
|
|
223
|
+
def message_error?(exception)
|
|
224
|
+
(@protocol == :udp && exception.is_a?(Errno::EMSGSIZE)) || super
|
|
225
|
+
end
|
|
226
|
+
|
|
227
|
+
# Returns [SemanticLogger::Formatters::Base] default formatter for this Appender
|
|
228
|
+
# depending on the protocol selected
|
|
214
229
|
def default_formatter
|
|
215
230
|
if protocol == :syslog
|
|
216
231
|
# Format is text output without the time.
|
|
217
232
|
# Escape control characters so untrusted log data cannot forge syslog entries.
|
|
218
|
-
|
|
233
|
+
# Exception details are off, as with the remote syslog formatters, so existing records do not change.
|
|
234
|
+
SemanticLogger::Formatters::Default.new(
|
|
235
|
+
time_format: :notime, escape_control_chars: true, log_exception_details: false
|
|
236
|
+
)
|
|
219
237
|
else
|
|
220
238
|
SemanticLogger::Formatters::Syslog.new(facility: facility, level_map: level_map, max_size: max_size)
|
|
221
239
|
end
|
|
222
240
|
end
|
|
241
|
+
|
|
242
|
+
# The formatters that build remote syslog packets, and so need this appender's facility, level map,
|
|
243
|
+
# and maximum packet size.
|
|
244
|
+
PACKET_FORMATTERS = %i[syslog syslog_cee].freeze
|
|
245
|
+
private_constant :PACKET_FORMATTERS
|
|
246
|
+
|
|
247
|
+
# The url without any credentials. See Subscriber#inspect.
|
|
248
|
+
def inspect_details
|
|
249
|
+
"url=#{Utils.redacted_url(url).inspect}"
|
|
250
|
+
end
|
|
251
|
+
|
|
252
|
+
private
|
|
253
|
+
|
|
254
|
+
# Returns the supplied `formatter:` argument, with this appender's facility, level map, and
|
|
255
|
+
# maximum packet size added when it names one of the PACKET_FORMATTERS, so that they are not
|
|
256
|
+
# silently ignored. Options in a
|
|
257
|
+
# `{syslog: {...}}` Hash take precedence. A formatter instance keeps its own settings.
|
|
258
|
+
def with_packet_options(formatter)
|
|
259
|
+
name, options = formatter.is_a?(Hash) ? formatter.first : [formatter, {}]
|
|
260
|
+
return formatter unless name.respond_to?(:to_sym) && PACKET_FORMATTERS.include?(name.to_sym)
|
|
261
|
+
|
|
262
|
+
{name.to_sym => {facility: facility, level_map: level_map, max_size: max_size}.merge(options || {})}
|
|
263
|
+
end
|
|
223
264
|
end
|
|
224
265
|
end
|
|
225
266
|
end
|
|
@@ -183,8 +183,8 @@ module SemanticLogger
|
|
|
183
183
|
# connect_retry_count: 5
|
|
184
184
|
# )
|
|
185
185
|
def initialize(separator: "\n",
|
|
186
|
-
level: nil, formatter: nil, filter: nil, application: nil, environment: nil,
|
|
187
|
-
**tcp_client_args, &)
|
|
186
|
+
level: nil, formatter: nil, filter: nil, application: nil, environment: nil,
|
|
187
|
+
host: nil, metrics: false, **tcp_client_args, &)
|
|
188
188
|
@separator = separator
|
|
189
189
|
@tcp_client_args = tcp_client_args
|
|
190
190
|
|
|
@@ -192,7 +192,8 @@ 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,
|
|
195
|
+
super(level: level, formatter: formatter, filter: filter, application: application,
|
|
196
|
+
environment: environment, host: host, metrics: metrics, &)
|
|
196
197
|
reopen
|
|
197
198
|
end
|
|
198
199
|
|
|
@@ -204,7 +205,7 @@ module SemanticLogger
|
|
|
204
205
|
|
|
205
206
|
# Write the log using the specified protocol and server.
|
|
206
207
|
def log(log)
|
|
207
|
-
message =
|
|
208
|
+
message = format_log(log)
|
|
208
209
|
@tcp_client.retry_on_connection_failure do
|
|
209
210
|
@tcp_client.write("#{message}#{separator}")
|
|
210
211
|
end
|
|
@@ -221,6 +222,11 @@ module SemanticLogger
|
|
|
221
222
|
@tcp_client&.close
|
|
222
223
|
end
|
|
223
224
|
|
|
225
|
+
# Only the servers: the other options can hold TLS keys and passwords. See Subscriber#inspect.
|
|
226
|
+
def inspect_details
|
|
227
|
+
"server=#{(@tcp_client_args[:server] || @tcp_client_args[:servers]).inspect}"
|
|
228
|
+
end
|
|
229
|
+
|
|
224
230
|
private
|
|
225
231
|
|
|
226
232
|
# Returns [SemanticLogger::Formatters::Default] formatter default for this Appender
|
|
@@ -80,7 +80,7 @@ module SemanticLogger
|
|
|
80
80
|
|
|
81
81
|
# Write the log using the specified protocol and server.
|
|
82
82
|
def log(log)
|
|
83
|
-
@socket.send(
|
|
83
|
+
@socket.send(format_log(log), udp_flags)
|
|
84
84
|
true
|
|
85
85
|
end
|
|
86
86
|
|
|
@@ -94,6 +94,16 @@ module SemanticLogger
|
|
|
94
94
|
@socket&.close
|
|
95
95
|
end
|
|
96
96
|
|
|
97
|
+
# A log message too large for a datagram (Errno::EMSGSIZE) cannot be sent by retrying.
|
|
98
|
+
def message_error?(exception)
|
|
99
|
+
exception.is_a?(Errno::EMSGSIZE) || super
|
|
100
|
+
end
|
|
101
|
+
|
|
102
|
+
# See Subscriber#inspect.
|
|
103
|
+
def inspect_details
|
|
104
|
+
"server=#{server.inspect}"
|
|
105
|
+
end
|
|
106
|
+
|
|
97
107
|
private
|
|
98
108
|
|
|
99
109
|
# Returns [SemanticLogger::Formatters::Default] formatter default for this Appender
|
|
@@ -45,7 +45,8 @@ module SemanticLogger
|
|
|
45
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}.
|
|
48
|
+
"Supplied logger does not implement:#{does_not_implement}. " \
|
|
49
|
+
"It must implement all of #{LEVELS[1..].inspect}")
|
|
49
50
|
end
|
|
50
51
|
|
|
51
52
|
super(**args, &)
|
|
@@ -56,7 +57,7 @@ module SemanticLogger
|
|
|
56
57
|
# Ruby or Rails Loggers
|
|
57
58
|
def log(log)
|
|
58
59
|
level = log.level == :trace ? :debug : log.level
|
|
59
|
-
@logger.send(level,
|
|
60
|
+
@logger.send(level, format_log(log))
|
|
60
61
|
true
|
|
61
62
|
end
|
|
62
63
|
|
|
@@ -87,7 +87,8 @@ module SemanticLogger
|
|
|
87
87
|
appender
|
|
88
88
|
else
|
|
89
89
|
raise(ArgumentError,
|
|
90
|
-
"Parameter :appender must be either a Symbol or an object derived from
|
|
90
|
+
"Parameter :appender must be either a Symbol or an object derived from " \
|
|
91
|
+
"SemanticLogger::Subscriber, not: #{appender.inspect}")
|
|
91
92
|
end
|
|
92
93
|
elsif metric
|
|
93
94
|
if metric.is_a?(Symbol)
|
|
@@ -96,11 +97,13 @@ module SemanticLogger
|
|
|
96
97
|
metric
|
|
97
98
|
else
|
|
98
99
|
raise(ArgumentError,
|
|
99
|
-
"Parameter :metric must be either a Symbol or an object derived from
|
|
100
|
+
"Parameter :metric must be either a Symbol or an object derived from " \
|
|
101
|
+
"SemanticLogger::Subscriber, not: #{appender.inspect}")
|
|
100
102
|
end
|
|
101
103
|
else
|
|
102
104
|
raise(ArgumentError,
|
|
103
|
-
"To create an appender it must supply one of the following:
|
|
105
|
+
"To create an appender it must supply one of the following: " \
|
|
106
|
+
":io, :file_name, :appender, :metric, or :logger")
|
|
104
107
|
end
|
|
105
108
|
end
|
|
106
109
|
|
|
@@ -14,7 +14,8 @@ module SemanticLogger
|
|
|
14
14
|
|
|
15
15
|
stream = appender.respond_to?(:console_stream) && appender.console_stream
|
|
16
16
|
if stream && console_streams.include?(stream)
|
|
17
|
-
logger.warn "Ignoring attempt to add a second #{stream} console appender since it
|
|
17
|
+
logger.warn "Ignoring attempt to add a second #{stream} console appender since it " \
|
|
18
|
+
"would result in duplicate console output."
|
|
18
19
|
return
|
|
19
20
|
end
|
|
20
21
|
|
|
@@ -36,7 +37,7 @@ module SemanticLogger
|
|
|
36
37
|
# Returns [Array<Hash>] operational statistics for each appender.
|
|
37
38
|
#
|
|
38
39
|
# 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
|
+
# their queue size and processed/dropped/failed counts. Appenders that log inline on the
|
|
40
41
|
# processor thread report only their name with `async: false`.
|
|
41
42
|
def stats
|
|
42
43
|
map do |appender|
|
|
@@ -48,9 +49,17 @@ module SemanticLogger
|
|
|
48
49
|
end
|
|
49
50
|
end
|
|
50
51
|
|
|
52
|
+
# Write the log message to every appender that should log it. An appender that fails is
|
|
53
|
+
# reported, and the others still write the message.
|
|
54
|
+
#
|
|
55
|
+
# A stack overflow is not a failure of the appender but of the thread, which must not overflow
|
|
56
|
+
# again: on macOS a second stack overflow on the same thread crashes Ruby. It is raised to the
|
|
57
|
+
# processor running this thread instead, and the message goes to no further appenders.
|
|
51
58
|
def log(log)
|
|
52
59
|
each do |appender|
|
|
53
60
|
appender.log(log) if appender.should_log?(log)
|
|
61
|
+
rescue SystemStackError
|
|
62
|
+
raise
|
|
54
63
|
rescue Exception => e
|
|
55
64
|
logger.error "Failed to log to appender: #{appender.name}", e
|
|
56
65
|
end
|