semantic_logger 4.7.2 → 4.10.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 (35) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +50 -20
  3. data/lib/semantic_logger/appender/async.rb +1 -1
  4. data/lib/semantic_logger/appender/async_batch.rb +3 -1
  5. data/lib/semantic_logger/appender/bugsnag.rb +36 -23
  6. data/lib/semantic_logger/appender/elasticsearch.rb +24 -6
  7. data/lib/semantic_logger/appender/file.rb +249 -68
  8. data/lib/semantic_logger/appender/io.rb +68 -0
  9. data/lib/semantic_logger/appender/kafka.rb +4 -0
  10. data/lib/semantic_logger/appender/sentry_ruby.rb +138 -0
  11. data/lib/semantic_logger/appender/splunk.rb +2 -1
  12. data/lib/semantic_logger/appender/splunk_http.rb +3 -2
  13. data/lib/semantic_logger/appender/syslog.rb +5 -3
  14. data/lib/semantic_logger/appender/wrapper.rb +3 -2
  15. data/lib/semantic_logger/appender.rb +13 -6
  16. data/lib/semantic_logger/appenders.rb +31 -27
  17. data/lib/semantic_logger/base.rb +16 -7
  18. data/lib/semantic_logger/formatters/base.rb +1 -0
  19. data/lib/semantic_logger/formatters/color.rb +3 -3
  20. data/lib/semantic_logger/formatters/logfmt.rb +72 -0
  21. data/lib/semantic_logger/formatters/syslog.rb +2 -1
  22. data/lib/semantic_logger/formatters/syslog_cee.rb +2 -1
  23. data/lib/semantic_logger/formatters.rb +10 -11
  24. data/lib/semantic_logger/log.rb +2 -4
  25. data/lib/semantic_logger/loggable.rb +8 -1
  26. data/lib/semantic_logger/logger.rb +16 -6
  27. data/lib/semantic_logger/processor.rb +3 -3
  28. data/lib/semantic_logger/semantic_logger.rb +18 -10
  29. data/lib/semantic_logger/subscriber.rb +10 -0
  30. data/lib/semantic_logger/sync_processor.rb +5 -5
  31. data/lib/semantic_logger/test/capture_log_events.rb +34 -0
  32. data/lib/semantic_logger/utils.rb +29 -10
  33. data/lib/semantic_logger/version.rb +1 -1
  34. data/lib/semantic_logger.rb +4 -0
  35. metadata +13 -10
@@ -0,0 +1,68 @@
1
+ # File appender
2
+ #
3
+ # Writes log messages to a file or open iostream
4
+ #
5
+ module SemanticLogger
6
+ module Appender
7
+ class IO < SemanticLogger::Subscriber
8
+ # Create a Stream Logger appender instance.
9
+ #
10
+ # Parameters
11
+ # io [IO]
12
+ # An IO stream to which to write the log messages to.
13
+ #
14
+ # :level [:trace | :debug | :info | :warn | :error | :fatal]
15
+ # Override the log level for this appender.
16
+ # Default: SemanticLogger.default_level
17
+ #
18
+ # :formatter: [Object|Proc]
19
+ # An instance of a class that implements #call, or a Proc to be used to format
20
+ # the output from this appender
21
+ # Default: Use the built-in formatter (See: #call)
22
+ #
23
+ # :filter [Regexp|Proc]
24
+ # RegExp: Only include log messages where the class name matches the supplied
25
+ # regular expression. All other messages will be ignored.
26
+ # Proc: Only include log messages where the supplied Proc returns true
27
+ # The Proc must return true or false.
28
+ #
29
+ # Example
30
+ # require "semantic_logger"
31
+ #
32
+ # # Enable trace level logging
33
+ # SemanticLogger.default_level = :info
34
+ #
35
+ # # Log to screen
36
+ # SemanticLogger.add_appender(io: $stdout, formatter: :color)
37
+ #
38
+ # logger = SemanticLogger['test']
39
+ # logger.info 'Hello World'
40
+ def initialize(io, **args, &block)
41
+ @io = io
42
+ unless @io.respond_to?(:write)
43
+ raise(ArgumentError, "SemanticLogging::Appender::IO io is not a valid IO instance: #{io.inspect}")
44
+ end
45
+
46
+ super(**args, &block)
47
+ end
48
+
49
+ def log(log)
50
+ # Since only one appender thread will be writing to the file at a time
51
+ # it is not necessary to protect access to the file with a semaphore
52
+ # Allow this logger to filter out log levels lower than it's own
53
+ @io.write(formatter.call(log, self) << "\n")
54
+ true
55
+ end
56
+
57
+ # Flush all pending logs to disk.
58
+ # Waits for all sent documents to be written to disk
59
+ def flush
60
+ @io.flush if @io.respond_to?(:flush)
61
+ end
62
+
63
+ def console_output?
64
+ [$stderr, $stdout].include?(@io)
65
+ end
66
+ end
67
+ end
68
+ end
@@ -183,6 +183,10 @@ module SemanticLogger
183
183
  delivery_interval: delivery_interval
184
184
  )
185
185
  end
186
+
187
+ private
188
+
189
+ attr_reader :producer
186
190
  end
187
191
  end
188
192
  end
@@ -0,0 +1,138 @@
1
+ begin
2
+ require "sentry-ruby"
3
+ rescue LoadError
4
+ raise LoadError, 'Gem sentry-ruby is required for logging purposes. Please add the gem "sentry-ruby" to your Gemfile.'
5
+ end
6
+
7
+ # Send log messages to sentry
8
+ #
9
+ # Example:
10
+ # SemanticLogger.add_appender(appender: :sentry_ruby)
11
+ #
12
+ module SemanticLogger
13
+ module Appender
14
+ class SentryRuby < SemanticLogger::Subscriber
15
+ # Create Appender
16
+ #
17
+ # Parameters
18
+ # level: [:trace | :debug | :info | :warn | :error | :fatal]
19
+ # Override the log level for this appender.
20
+ # Default: :error
21
+ #
22
+ # formatter: [Object|Proc|Symbol|Hash]
23
+ # An instance of a class that implements #call, or a Proc to be used to format
24
+ # the output from this appender
25
+ # Default: Use the built-in formatter (See: #call)
26
+ #
27
+ # filter: [Regexp|Proc]
28
+ # RegExp: Only include log messages where the class name matches the supplied.
29
+ # regular expression. All other messages will be ignored.
30
+ # Proc: Only include log messages where the supplied Proc returns true
31
+ # The Proc must return true or false.
32
+ #
33
+ # host: [String]
34
+ # Name of this host to appear in log messages.
35
+ # Default: SemanticLogger.host
36
+ #
37
+ # application: [String]
38
+ # Name of this application to appear in log messages.
39
+ # Default: SemanticLogger.application
40
+ def initialize(level: :error, **args, &block)
41
+ # Replace the Sentry Ruby logger so that we can identify its log
42
+ # messages and not forward them to Sentry
43
+ ::Sentry.init { |config| config.logger = SemanticLogger[::Sentry] }
44
+ super(level: level, **args, &block)
45
+ end
46
+
47
+ # Send an error notification to sentry
48
+ def log(log)
49
+ # Ignore logs coming from Sentry itself
50
+ return false if log.name == "Sentry"
51
+
52
+ context = formatter.call(log, self)
53
+ payload = context.delete(:payload) || {}
54
+ named_tags = context[:named_tags] || {}
55
+ transaction_name = named_tags.delete(:transaction_name)
56
+
57
+ user = extract_user!(named_tags, payload)
58
+ tags = extract_tags!(context)
59
+
60
+ fingerprint = payload.delete(:fingerprint)
61
+
62
+ ::Sentry.with_scope do |scope|
63
+ scope.set_user(user) if user
64
+ scope.set_level(context.delete(:level)) if context[:level]
65
+ scope.set_fingerprint(fingerprint) if fingerprint
66
+ scope.set_transaction_name(transaction_name) if transaction_name
67
+ scope.set_tags(tags)
68
+ scope.set_extras(context)
69
+ scope.set_extras(payload)
70
+
71
+ if log.exception
72
+ ::Sentry.capture_exception(log.exception)
73
+ elsif log.backtrace
74
+ ::Sentry.capture_message(context[:message], backtrace: log.backtrace)
75
+ else
76
+ ::Sentry.capture_message(context[:message])
77
+ end
78
+ end
79
+
80
+ true
81
+ end
82
+
83
+ private
84
+
85
+ # Use Raw Formatter by default
86
+ def default_formatter
87
+ SemanticLogger::Formatters::Raw.new
88
+ end
89
+
90
+ # Extract user data from named tags or payload.
91
+ #
92
+ # Keys :user_id and :user_email will be used as :id and :email respectively.
93
+ # Keys :username and :ip_address will be used verbatim.
94
+ #
95
+ # Any additional value nested in a :user key will be added, provided any of
96
+ # the above keys is already present.
97
+ #
98
+ def extract_user!(*sources)
99
+ keys = {user_id: :id, username: :username, user_email: :email, ip_address: :ip_address}
100
+
101
+ user = {}
102
+
103
+ sources.each do |source|
104
+ keys.each do |source_key, target_key|
105
+ value = source.delete(source_key)
106
+ user[target_key] = value if value
107
+ end
108
+ end
109
+
110
+ return if user.empty?
111
+
112
+ sources.each do |source|
113
+ extras = source.delete(:user)
114
+ user.merge!(extras) if extras.is_a?(Hash)
115
+ end
116
+
117
+ user
118
+ end
119
+
120
+ # Extract tags.
121
+ #
122
+ # Named tags will be stringified (both key and value).
123
+ # Unnamed tags will be stringified and joined with a comma. Then they will
124
+ # be used as a "tag" named tag. If such a tag already exists, it is also
125
+ # joined with a comma.
126
+ #
127
+ # Finally, the tag names are limited to 32 characters and the tag values to 256.
128
+ #
129
+ def extract_tags!(context)
130
+ named_tags = context.delete(:named_tags) || {}
131
+ named_tags = named_tags.map { |k, v| [k.to_s, v.to_s] }.to_h
132
+ tags = context.delete(:tags)
133
+ named_tags.merge!("tag" => tags.join(", ")) { |_, v1, v2| "#{v1}, #{v2}" } if tags
134
+ named_tags.map { |k, v| [k[0...32], v[0...256]] }.to_h
135
+ end
136
+ end
137
+ end
138
+ end
@@ -1,7 +1,8 @@
1
1
  begin
2
2
  require "splunk-sdk-ruby"
3
3
  rescue LoadError
4
- raise LoadError, 'Gem splunk-sdk-ruby is required for logging to Splunk. Please add the gem "splunk-sdk-ruby" to your Gemfile.'
4
+ raise LoadError,
5
+ 'Gem splunk-sdk-ruby is required for logging to Splunk. Please add the gem "splunk-sdk-ruby" to your Gemfile.'
5
6
  end
6
7
 
7
8
  # Splunk log appender.
@@ -89,8 +89,9 @@ module SemanticLogger
89
89
  # For splunk format requirements see:
90
90
  # https://docs.splunk.com/Documentation/Splunk/latest/Data/FormateventsforHTTPEventCollector
91
91
  def call(log, logger)
92
- h = SemanticLogger::Formatters::Raw.new(time_format: :seconds).call(log, logger)
93
- message = {
92
+ h = SemanticLogger::Formatters::Raw.new(time_format: :seconds).call(log, logger)
93
+ h.delete(:host)
94
+ message = {
94
95
  source: logger.application,
95
96
  host: logger.host,
96
97
  time: h.delete(:time),
@@ -57,7 +57,7 @@ module SemanticLogger
57
57
  # Only used with the TCP protocol.
58
58
  # Specify custom parameters to pass into Net::TCPClient.new
59
59
  # For a list of options see the net_tcp_client documentation:
60
- # https://github.com/rocketjob/net_tcp_client/blob/master/lib/net/tcp_client/tcp_client.rb
60
+ # https://github.com/reidmorrison/net_tcp_client/blob/master/lib/net/tcp_client/tcp_client.rb
61
61
  #
62
62
  # level: [:trace | :debug | :info | :warn | :error | :fatal]
63
63
  # Override the log level for this appender.
@@ -151,7 +151,8 @@ module SemanticLogger
151
151
  begin
152
152
  require "syslog_protocol"
153
153
  rescue LoadError
154
- raise LoadError, "Missing gem: syslog_protocol. This gem is required when logging over TCP or UDP. To fix this error: gem install syslog_protocol"
154
+ raise LoadError,
155
+ "Missing gem: syslog_protocol. This gem is required when logging over TCP or UDP. To fix this error: gem install syslog_protocol"
155
156
  end
156
157
 
157
158
  # The net_tcp_client gem is required when logging over TCP.
@@ -159,7 +160,8 @@ module SemanticLogger
159
160
  begin
160
161
  require "net/tcp_client"
161
162
  rescue LoadError
162
- raise LoadError, "Missing gem: net_tcp_client. This gem is required when logging over TCP. To fix this error: gem install net_tcp_client"
163
+ raise LoadError,
164
+ "Missing gem: net_tcp_client. This gem is required when logging over TCP. To fix this error: gem install net_tcp_client"
163
165
  end
164
166
  end
165
167
  end
@@ -32,7 +32,7 @@ module SemanticLogger
32
32
  # require 'logger'
33
33
  # require 'semantic_logger'
34
34
  #
35
- # ruby_logger = Logger.new(STDOUT)
35
+ # ruby_logger = Logger.new($stdout)
36
36
  # SemanticLogger.add_appender(logger: ruby_logger)
37
37
  #
38
38
  # logger = SemanticLogger['test']
@@ -45,7 +45,8 @@ module SemanticLogger
45
45
  # Check if the custom appender responds to all the log levels. For example Ruby ::Logger
46
46
  does_not_implement = LEVELS[1..-1].find { |i| !@logger.respond_to?(i) }
47
47
  if does_not_implement
48
- raise(ArgumentError, "Supplied logger does not implement:#{does_not_implement}. It must implement all of #{LEVELS[1..-1].inspect}")
48
+ raise(ArgumentError,
49
+ "Supplied logger does not implement:#{does_not_implement}. It must implement all of #{LEVELS[1..-1].inspect}")
49
50
  end
50
51
 
51
52
  super(**args, &block)
@@ -9,6 +9,7 @@ module SemanticLogger
9
9
  autoload :File, "semantic_logger/appender/file"
10
10
  autoload :Graylog, "semantic_logger/appender/graylog"
11
11
  autoload :Honeybadger, "semantic_logger/appender/honeybadger"
12
+ autoload :IO, "semantic_logger/appender/io"
12
13
  autoload :Kafka, "semantic_logger/appender/kafka"
13
14
  autoload :Sentry, "semantic_logger/appender/sentry"
14
15
  autoload :Http, "semantic_logger/appender/http"
@@ -21,6 +22,7 @@ module SemanticLogger
21
22
  autoload :Tcp, "semantic_logger/appender/tcp"
22
23
  autoload :Udp, "semantic_logger/appender/udp"
23
24
  autoload :Wrapper, "semantic_logger/appender/wrapper"
25
+ autoload :SentryRuby, "semantic_logger/appender/sentry_ruby"
24
26
  # @formatter:on
25
27
 
26
28
  # Returns [SemanticLogger::Subscriber] appender for the supplied options
@@ -32,7 +34,7 @@ module SemanticLogger
32
34
  appender = build(**args, &block)
33
35
 
34
36
  # If appender implements #batch, then it should use the batch proxy by default.
35
- batch = true if batch.nil? && appender.respond_to?(:batch)
37
+ batch = true if batch.nil? && appender.respond_to?(:batch)
36
38
 
37
39
  if batch == true
38
40
  Appender::AsyncBatch.new(
@@ -56,8 +58,10 @@ module SemanticLogger
56
58
 
57
59
  # Returns [Subscriber] instance from the supplied options.
58
60
  def self.build(io: nil, file_name: nil, appender: nil, metric: nil, logger: nil, **args, &block)
59
- if io || file_name
60
- SemanticLogger::Appender::File.new(io: io, file_name: file_name, **args, &block)
61
+ if file_name
62
+ SemanticLogger::Appender::File.new(file_name, **args, &block)
63
+ elsif io
64
+ SemanticLogger::Appender::IO.new(io, **args, &block)
61
65
  elsif logger
62
66
  SemanticLogger::Appender::Wrapper.new(logger: logger, **args, &block)
63
67
  elsif appender
@@ -66,7 +70,8 @@ module SemanticLogger
66
70
  elsif appender.is_a?(Subscriber)
67
71
  appender
68
72
  else
69
- raise(ArgumentError, "Parameter :appender must be either a Symbol or an object derived from SemanticLogger::Subscriber, not: #{appender.inspect}")
73
+ raise(ArgumentError,
74
+ "Parameter :appender must be either a Symbol or an object derived from SemanticLogger::Subscriber, not: #{appender.inspect}")
70
75
  end
71
76
  elsif metric
72
77
  if metric.is_a?(Symbol)
@@ -74,10 +79,12 @@ module SemanticLogger
74
79
  elsif metric.is_a?(Subscriber)
75
80
  metric
76
81
  else
77
- raise(ArgumentError, "Parameter :metric must be either a Symbol or an object derived from SemanticLogger::Subscriber, not: #{appender.inspect}")
82
+ raise(ArgumentError,
83
+ "Parameter :metric must be either a Symbol or an object derived from SemanticLogger::Subscriber, not: #{appender.inspect}")
78
84
  end
79
85
  else
80
- raise(ArgumentError, "To create an appender it must supply one of the following: :io, :file_name, :appender, :metric, or :logger")
86
+ raise(ArgumentError,
87
+ "To create an appender it must supply one of the following: :io, :file_name, :appender, :metric, or :logger")
81
88
  end
82
89
  end
83
90
 
@@ -10,42 +10,48 @@ module SemanticLogger
10
10
 
11
11
  def add(**args, &block)
12
12
  appender = SemanticLogger::Appender.factory(**args, &block)
13
+
14
+ if appender.respond_to?(:console_output?) && appender.console_output? && console_output?
15
+ logger.warn "Ignoring attempt to add a second console appender: #{appender.class.name} since it would result in duplicate console output."
16
+ return
17
+ end
18
+
13
19
  self << appender
14
20
  appender
15
21
  end
16
22
 
23
+ # Whether any of the existing appenders already output to the console?
24
+ # I.e. Writes to stdout or stderr.
25
+ def console_output?
26
+ any? { |appender| appender.respond_to?(:console_output?) && appender.console_output? }
27
+ end
28
+
17
29
  def log(log)
18
30
  each do |appender|
19
- begin
20
- appender.log(log) if appender.should_log?(log)
21
- rescue Exception => e
22
- logger.error "Failed to log to appender: #{appender.name}", e
23
- end
31
+ appender.log(log) if appender.should_log?(log)
32
+ rescue Exception => e
33
+ logger.error "Failed to log to appender: #{appender.name}", e
24
34
  end
25
35
  end
26
36
 
27
37
  def flush
28
38
  each do |appender|
29
- begin
30
- logger.trace "Flushing appender: #{appender.name}"
31
- appender.flush
32
- rescue Exception => e
33
- logger.error "Failed to flush appender: #{appender.name}", e
34
- end
39
+ logger.trace "Flushing appender: #{appender.name}"
40
+ appender.flush
41
+ rescue Exception => e
42
+ logger.error "Failed to flush appender: #{appender.name}", e
35
43
  end
36
44
  logger.trace "All appenders flushed"
37
45
  end
38
46
 
39
47
  def close
40
- each do |appender|
41
- begin
42
- logger.trace "Closing appender: #{appender.name}"
43
- appender.flush
44
- appender.close
45
- delete(appender)
46
- rescue Exception => e
47
- logger.error "Failed to close appender: #{appender.name}", e
48
- end
48
+ to_a.each do |appender|
49
+ logger.trace "Closing appender: #{appender.name}"
50
+ delete(appender)
51
+ appender.flush
52
+ appender.close
53
+ rescue Exception => e
54
+ logger.error "Failed to close appender: #{appender.name}", e
49
55
  end
50
56
  logger.trace "All appenders closed and removed from appender list"
51
57
  end
@@ -53,14 +59,12 @@ module SemanticLogger
53
59
  # After a fork the appender thread is not running, start it if it is not running.
54
60
  def reopen
55
61
  each do |appender|
56
- begin
57
- next unless appender.respond_to?(:reopen)
62
+ next unless appender.respond_to?(:reopen)
58
63
 
59
- logger.trace "Reopening appender: #{appender.name}"
60
- appender.reopen
61
- rescue Exception => e
62
- logger.error "Failed to re-open appender: #{appender.name}", e
63
- end
64
+ logger.trace "Reopening appender: #{appender.name}"
65
+ appender.reopen
66
+ rescue Exception => e
67
+ logger.error "Failed to re-open appender: #{appender.name}", e
64
68
  end
65
69
  logger.trace "All appenders re-opened"
66
70
  end
@@ -63,7 +63,7 @@ module SemanticLogger
63
63
  # SemanticLogger.default_level = :info
64
64
  #
65
65
  # # Log to screen
66
- # SemanticLogger.add_appender(io: STDOUT, formatter: :color)
66
+ # SemanticLogger.add_appender(io: $stdout, formatter: :color)
67
67
  #
68
68
  # # And log to a file at the same time
69
69
  # SemanticLogger.add_appender(file_name: 'application.log', formatter: :color)
@@ -136,7 +136,7 @@ module SemanticLogger
136
136
 
137
137
  backtrace =
138
138
  if thread == Thread.current
139
- Utils.extract_backtrace
139
+ Utils.extract_backtrace(caller)
140
140
  else
141
141
  log.thread_name = thread.name
142
142
  log.tags = (thread[:semantic_logger_tags] || []).clone
@@ -188,7 +188,8 @@ module SemanticLogger
188
188
  # - For better performance with clean tags, see `SemanticLogger.tagged`.
189
189
  def tagged(*tags, &block)
190
190
  # Allow named tags to be passed into the logger
191
- if tags.size == 1
191
+ # Rails::Rack::Logger passes logs as an array with a single argument
192
+ if tags.size == 1 && !tags.first.is_a?(Array)
192
193
  tag = tags[0]
193
194
  return yield if tag.nil? || tag == ""
194
195
 
@@ -263,14 +264,22 @@ module SemanticLogger
263
264
  # For example if set to :warn, this appender would only log :warn and :fatal
264
265
  # log messages when other appenders could be logging :info and lower
265
266
  #
266
- # filter [Regexp|Proc]
267
+ # filter [Regexp|Proc|Module]
267
268
  # RegExp: Only include log messages where the class name matches the supplied
268
269
  # regular expression. All other messages will be ignored
269
270
  # Proc: Only include log messages where the supplied Proc returns true
270
271
  # The Proc must return true or false
272
+ # Module: A module that implements `.call`. For example:
273
+ # module ComplexFilter
274
+ # def self.call(log)
275
+ # (/\AExclude/ =~ log.message).nil?
276
+ # end
277
+ # end
271
278
  def initialize(klass, level = nil, filter = nil)
272
- # Support filtering all messages to this logger using a Regular Expression or Proc
273
- raise ":filter must be a Regexp or Proc" unless filter.nil? || filter.is_a?(Regexp) || filter.is_a?(Proc)
279
+ # Support filtering all messages to this logger instance.
280
+ unless filter.nil? || filter.is_a?(Regexp) || filter.is_a?(Proc) || filter.respond_to?(:call)
281
+ raise ":filter must be a Regexp, Proc, or implement :call"
282
+ end
274
283
 
275
284
  @filter = filter.is_a?(Regexp) ? filter.freeze : filter
276
285
  @name = klass.is_a?(String) ? klass : klass.name
@@ -368,7 +377,7 @@ module SemanticLogger
368
377
  exception = e
369
378
  ensure
370
379
  # Must use ensure block otherwise a `return` in the yield above will skip the log entry
371
- log = Log.new(name, level, index)
380
+ log = Log.new(name, level, index)
372
381
  exception ||= params[:exception]
373
382
  message = params[:message] if params[:message]
374
383
  duration =
@@ -70,6 +70,7 @@ module SemanticLogger
70
70
 
71
71
  # Return the Time as a formatted string
72
72
  def format_time(time)
73
+ time = time.dup
73
74
  case time_format
74
75
  when :rfc_3339
75
76
  time.utc.to_datetime.rfc3339
@@ -109,14 +109,14 @@ module SemanticLogger
109
109
  def payload
110
110
  return unless log.payload?
111
111
 
112
- if !log.payload.respond_to?(:ai)
113
- super
114
- else
112
+ if log.payload.respond_to?(:ai)
115
113
  begin
116
114
  "-- #{log.payload.ai(@ai_options)}"
117
115
  rescue StandardError
118
116
  super
119
117
  end
118
+ else
119
+ super
120
120
  end
121
121
  end
122
122
 
@@ -0,0 +1,72 @@
1
+ require "json"
2
+
3
+ module SemanticLogger
4
+ module Formatters
5
+ # Produces logfmt formatted messages
6
+ #
7
+ # The following fields are extracted from the raw log and included in the formatted message:
8
+ # :timestamp, :level, :name, :message, :duration, :tags, :named_tags
9
+ #
10
+ # E.g.
11
+ # timestamp="2020-07-20T08:32:05.375276Z" level=info name="DefaultTest" base="breakfast" spaces="second breakfast" double_quotes="\"elevensies\"" single_quotes="'lunch'" tag="success"
12
+ #
13
+ # All timestamps are ISO8601 formatteed
14
+ # All user supplied values are escaped and surrounded by double quotes to avoid ambiguious message delimeters
15
+ # `tags` are treated as keys with boolean values. Tag names are not formatted or validated, ensure you use valid logfmt format for tag names.
16
+ # `named_tags` are flattened are merged into the top level message field. Any conflicting fields are overridden.
17
+ # `payload` values take precedence over `tags` and `named_tags`. Any conflicting fields are overridden.
18
+ #
19
+ # Futher Reading https://brandur.org/logfmt
20
+ class Logfmt < Raw
21
+ def initialize(time_format: :iso_8601, time_key: :timestamp, **args)
22
+ super(time_format: time_format, time_key: time_key, **args)
23
+ end
24
+
25
+ def call(log, logger)
26
+ @raw = super(log, logger)
27
+
28
+ raw_to_logfmt
29
+ end
30
+
31
+ private
32
+
33
+ def raw_to_logfmt
34
+ @parsed = @raw.slice(time_key, :level, :name, :message, :duration).merge(tag: "success")
35
+ handle_tags
36
+ handle_payload
37
+ handle_exception
38
+
39
+ flatten_log
40
+ end
41
+
42
+ def handle_tags
43
+ tags = @raw.fetch(:tags){ [] }
44
+ .each_with_object({}){ |tag, accum| accum[tag] = true }
45
+
46
+ @parsed = @parsed.merge(tags)
47
+ .merge(@raw.fetch(:named_tags){ {} })
48
+ end
49
+
50
+ def handle_payload
51
+ return unless @raw.key? :payload
52
+
53
+ @parsed = @parsed.merge(@raw[:payload])
54
+ end
55
+
56
+ def handle_exception
57
+ return unless @raw.key? :exception
58
+
59
+ @parsed[:tag] = "exception"
60
+ @parsed = @parsed.merge(@raw[:exception])
61
+ end
62
+
63
+ def flatten_log
64
+ flattened = @parsed.map do |key, value|
65
+ "#{key}=#{value.to_json}"
66
+ end
67
+
68
+ flattened.join(" ")
69
+ end
70
+ end
71
+ end
72
+ end
@@ -1,7 +1,8 @@
1
1
  begin
2
2
  require "syslog_protocol"
3
3
  rescue LoadError
4
- raise LoadError, 'Gem syslog_protocol is required for remote logging using the Syslog protocol. Please add the gem "syslog_protocol" to your Gemfile.'
4
+ raise LoadError,
5
+ 'Gem syslog_protocol is required for remote logging using the Syslog protocol. Please add the gem "syslog_protocol" to your Gemfile.'
5
6
  end
6
7
 
7
8
  module SemanticLogger
@@ -1,7 +1,8 @@
1
1
  begin
2
2
  require "syslog_protocol"
3
3
  rescue LoadError
4
- raise LoadError, 'Gem syslog_protocol is required for remote logging using the Syslog protocol. Please add the gem "syslog_protocol" to your Gemfile.'
4
+ raise LoadError,
5
+ 'Gem syslog_protocol is required for remote logging using the Syslog protocol. Please add the gem "syslog_protocol" to your Gemfile.'
5
6
  end
6
7
 
7
8
  module SemanticLogger
@@ -1,16 +1,15 @@
1
1
  module SemanticLogger
2
2
  module Formatters
3
- # @formatter:off
4
- autoload :Base, "semantic_logger/formatters/base"
5
- autoload :Color, "semantic_logger/formatters/color"
6
- autoload :Default, "semantic_logger/formatters/default"
7
- autoload :Json, "semantic_logger/formatters/json"
8
- autoload :Raw, "semantic_logger/formatters/raw"
9
- autoload :OneLine, "semantic_logger/formatters/one_line"
10
- autoload :Signalfx, "semantic_logger/formatters/signalfx"
11
- autoload :Syslog, "semantic_logger/formatters/syslog"
12
- autoload :Fluentd, "semantic_logger/formatters/fluentd"
13
- # @formatter:on
3
+ autoload :Base, "semantic_logger/formatters/base"
4
+ autoload :Color, "semantic_logger/formatters/color"
5
+ autoload :Default, "semantic_logger/formatters/default"
6
+ autoload :Json, "semantic_logger/formatters/json"
7
+ autoload :Raw, "semantic_logger/formatters/raw"
8
+ autoload :OneLine, "semantic_logger/formatters/one_line"
9
+ autoload :Signalfx, "semantic_logger/formatters/signalfx"
10
+ autoload :Syslog, "semantic_logger/formatters/syslog"
11
+ autoload :Fluentd, "semantic_logger/formatters/fluentd"
12
+ autoload :Logfmt, "semantic_logger/formatters/logfmt"
14
13
 
15
14
  # Return formatter that responds to call.
16
15
  #