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.
Files changed (75) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +20 -10
  3. data/Rakefile +40 -0
  4. data/docs/api.md +506 -0
  5. data/docs/appenders.md +1074 -0
  6. data/docs/config.md +707 -0
  7. data/docs/index.md +223 -0
  8. data/docs/log.md +116 -0
  9. data/docs/metrics.md +171 -0
  10. data/docs/operations.md +446 -0
  11. data/docs/rails.md +890 -0
  12. data/docs/security.md +286 -0
  13. data/docs/testing.md +287 -0
  14. data/docs/upgrading.md +697 -0
  15. data/lib/semantic_logger/appender/async.rb +18 -5
  16. data/lib/semantic_logger/appender/bugsnag.rb +1 -1
  17. data/lib/semantic_logger/appender/cloudwatch_logs.rb +126 -29
  18. data/lib/semantic_logger/appender/elasticsearch.rb +2 -5
  19. data/lib/semantic_logger/appender/elasticsearch_base.rb +48 -23
  20. data/lib/semantic_logger/appender/elasticsearch_http.rb +15 -8
  21. data/lib/semantic_logger/appender/file.rb +141 -28
  22. data/lib/semantic_logger/appender/graylog.rb +22 -6
  23. data/lib/semantic_logger/appender/honeybadger.rb +1 -1
  24. data/lib/semantic_logger/appender/honeybadger_insights.rb +1 -1
  25. data/lib/semantic_logger/appender/http.rb +70 -11
  26. data/lib/semantic_logger/appender/io.rb +1 -1
  27. data/lib/semantic_logger/appender/kafka.rb +26 -12
  28. data/lib/semantic_logger/appender/loki.rb +13 -3
  29. data/lib/semantic_logger/appender/mongodb.rb +20 -1
  30. data/lib/semantic_logger/appender/new_relic.rb +7 -1
  31. data/lib/semantic_logger/appender/new_relic_logs.rb +11 -12
  32. data/lib/semantic_logger/appender/open_telemetry.rb +4 -3
  33. data/lib/semantic_logger/appender/opensearch.rb +2 -1
  34. data/lib/semantic_logger/appender/rabbitmq.rb +10 -4
  35. data/lib/semantic_logger/appender/sentry.rb +6 -1
  36. data/lib/semantic_logger/appender/sentry_ruby.rb +91 -17
  37. data/lib/semantic_logger/appender/splunk.rb +10 -1
  38. data/lib/semantic_logger/appender/syslog.rb +66 -25
  39. data/lib/semantic_logger/appender/tcp.rb +10 -4
  40. data/lib/semantic_logger/appender/udp.rb +11 -1
  41. data/lib/semantic_logger/appender/wrapper.rb +3 -2
  42. data/lib/semantic_logger/appender.rb +6 -3
  43. data/lib/semantic_logger/appenders.rb +11 -2
  44. data/lib/semantic_logger/base.rb +82 -32
  45. data/lib/semantic_logger/exception_details.rb +50 -0
  46. data/lib/semantic_logger/formatters/base.rb +52 -6
  47. data/lib/semantic_logger/formatters/color.rb +22 -3
  48. data/lib/semantic_logger/formatters/default.rb +28 -6
  49. data/lib/semantic_logger/formatters/logfmt.rb +155 -21
  50. data/lib/semantic_logger/formatters/loki.rb +26 -22
  51. data/lib/semantic_logger/formatters/one_line.rb +67 -3
  52. data/lib/semantic_logger/formatters/open_telemetry.rb +16 -17
  53. data/lib/semantic_logger/formatters/pattern.rb +11 -5
  54. data/lib/semantic_logger/formatters/raw.rb +4 -5
  55. data/lib/semantic_logger/formatters/syslog.rb +110 -17
  56. data/lib/semantic_logger/formatters/syslog_cee.rb +29 -15
  57. data/lib/semantic_logger/levels.rb +10 -1
  58. data/lib/semantic_logger/log.rb +183 -37
  59. data/lib/semantic_logger/loggable.rb +26 -3
  60. data/lib/semantic_logger/metric/new_relic.rb +1 -1
  61. data/lib/semantic_logger/metric/signalfx.rb +12 -8
  62. data/lib/semantic_logger/metric/statsd.rb +15 -7
  63. data/lib/semantic_logger/queue_processor/request.rb +55 -0
  64. data/lib/semantic_logger/queue_processor/throttled_counter.rb +46 -0
  65. data/lib/semantic_logger/queue_processor.rb +170 -68
  66. data/lib/semantic_logger/reporters/minitest.rb +4 -4
  67. data/lib/semantic_logger/semantic_logger.rb +15 -2
  68. data/lib/semantic_logger/subscriber.rb +96 -10
  69. data/lib/semantic_logger/sync_processor.rb +11 -0
  70. data/lib/semantic_logger/test/capture_log_events.rb +9 -6
  71. data/lib/semantic_logger/utils.rb +244 -16
  72. data/lib/semantic_logger/version.rb +1 -1
  73. data/lib/semantic_logger.rb +1 -1
  74. metadata +33 -6
  75. 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 later in the formatted output.
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
- begin
60
- message = formatter.call(log, self) # 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
- rescue JSON::GeneratorError => e
65
- warn("Failed to serialize log message to JSON: #{e.message}")
66
- warn("Problematic data: #{message.inspect}")
67
- rescue StandardError => e
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
- 'Gem opentelemetry-logs-sdk is required for logging to Open Telemetry. Please add the gem "opentelemetry-logs-sdk" to your Gemfile.'
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 = formatter.call(log, self)
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 formatter.call(log, self)
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
- 'Gem opensearch-ruby is required for logging to OpenSearch. Please add the gem "opensearch-ruby" to your Gemfile.'
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, host: nil, metrics: true,
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, environment: environment, host: host, metrics: metrics, &)
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(formatter.call(log, self))
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 = formatter.call(log, self)
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 = formatter.call(log, self)
58
- payload = context.delete(:payload) || {}
59
- named_tags = context[:named_tags] || {}
60
- transaction_name = named_tags.delete(:transaction_name)
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
- def extract_user!(*sources)
104
- keys = {user_id: :id, username: :username, user_email: :email, ip_address: :ip_address}
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
- sources.each do |source|
109
- keys.each do |source_key, target_key|
110
- value = source.delete(source_key)
111
- user[target_key] = value if 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
- sources.each do |source|
118
- extras = source.delete(:user)
119
- user.merge!(extras) if extras.is_a?(Hash)
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
- user
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 = formatter.call(log, self)
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: 1024,
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: ::Syslog::LOG_PID | ::Syslog::LOG_CONS,
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 = URI(@url)
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 syslog_protocol gem is required when logging over TCP or UDP.
149
- if %i[tcp udp].include?(@protocol)
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 "syslog_protocol"
150
+ require "syslog"
152
151
  rescue LoadError
153
152
  raise LoadError,
154
- "Missing gem: syslog_protocol. This gem is required when logging over TCP or UDP. To fix this error: gem install syslog_protocol"
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
- # The net_tcp_client gem is required when logging over TCP.
158
- if protocol == :tcp
159
- begin
160
- require "net/tcp_client"
161
- rescue LoadError
162
- raise LoadError,
163
- "Missing gem: net_tcp_client. This gem is required when logging over TCP. To fix this error: gem install net_tcp_client"
164
- end
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 = formatter.call(log, self).gsub "%", "%%"
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("#{formatter.call(log, self)}\r\n") }
208
+ @remote_syslog.retry_on_connection_failure { @remote_syslog.write("#{format_log(log)}\r\n") }
200
209
  when :udp
201
- @remote_syslog.send(formatter.call(log, self), 0, @server, @port)
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
- # Returns [SemanticLogger::Formatters::Base] default formatter for this Appender depending on the protocal selected
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
- SemanticLogger::Formatters::Default.new(time_format: :notime, escape_control_chars: true)
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, host: nil, metrics: false,
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, environment: environment, host: host, metrics: metrics, &)
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 = formatter.call(log, self)
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(formatter.call(log, self), udp_flags)
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}. It must implement all of #{LEVELS[1..].inspect}")
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, formatter.call(log, self))
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 SemanticLogger::Subscriber, not: #{appender.inspect}")
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 SemanticLogger::Subscriber, not: #{appender.inspect}")
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: :io, :file_name, :appender, :metric, or :logger")
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 would result in duplicate console output."
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