Class: SemanticLogger::Log

Inherits:
Object
  • Object
show all
Defined in:
lib/semantic_logger/log.rb

Overview

Log

Class to hold all log entry information

level Log level of the supplied log call :trace, :debug, :info, :warn, :error, :fatal

thread_name Name of the thread in which the logging call was called

name Class name supplied to the logging instance

message Text message to be logged

payload Optional Hash or Ruby Exception object to be logged

time The time at which the log entry was created

duration The time taken to complete a measure call

tags Any tags active on the thread when the log call was made

level_index Internal index of the log level

exception Ruby Exception object to log

metric [Object] Object supplied when measure_x was called

backtrace [Array] The backtrace captured at source when the log level >= SemanticLogger.backtrace_level

metric_amount [Numeric] Used for numeric or counter metrics. For example, the number of inquiries or, the amount purchased etc.

context [Hash] Named contexts that were captured when the log entry was created.

Constant Summary collapse

NON_PAYLOAD_KEYS =

Keys passed in without a payload that will be extracted and the remainder passed into the payload.

%i[message exception backtrace exception
duration min_duration
log_exception on_exception_level
metric metric_amount dimensions].freeze
ASSIGNABLE_KEYS =

Keys that #assign_hash may assign directly to a log attribute. Every other key is folded into the payload, preventing a supplied hash from overwriting sensitive fields such as :level, :name, or :time, and keeping this path consistent with #extract_arguments.

(NON_PAYLOAD_KEYS + %i[payload]).freeze
MAX_EXCEPTIONS_TO_UNWRAP =
5
CALLER_REGEXP =
/^(.*):(\d+).*/

Instance Attribute Summary collapse

Instance Method Summary collapse

Constructor Details

#initialize(name, level, index = nil) ⇒ Log

Returns a new instance of Log.



61
62
63
64
65
66
67
68
69
# File 'lib/semantic_logger/log.rb', line 61

def initialize(name, level, index = nil)
  @level       = level
  @thread_name = Thread.current.name
  @name        = name
  @time        = Time.now
  @tags        = SemanticLogger.tags
  @named_tags  = SemanticLogger.named_tags
  @level_index = index.nil? ? Levels.index(level) : index
end

Instance Attribute Details

#backtraceObject

Returns the value of attribute backtrace.



56
57
58
# File 'lib/semantic_logger/log.rb', line 56

def backtrace
  @backtrace
end

#contextObject

Returns the value of attribute context.



56
57
58
# File 'lib/semantic_logger/log.rb', line 56

def context
  @context
end

#dimensionsObject

Returns the value of attribute dimensions.



56
57
58
# File 'lib/semantic_logger/log.rb', line 56

def dimensions
  @dimensions
end

#durationObject

Returns the value of attribute duration.



56
57
58
# File 'lib/semantic_logger/log.rb', line 56

def duration
  @duration
end

#exceptionObject

Returns the value of attribute exception.



56
57
58
# File 'lib/semantic_logger/log.rb', line 56

def exception
  @exception
end

#levelObject

Returns the value of attribute level.



56
57
58
# File 'lib/semantic_logger/log.rb', line 56

def level
  @level
end

#level_indexObject

Returns the value of attribute level_index.



56
57
58
# File 'lib/semantic_logger/log.rb', line 56

def level_index
  @level_index
end

#messageObject

Returns the value of attribute message.



56
57
58
# File 'lib/semantic_logger/log.rb', line 56

def message
  @message
end

#metricObject

Returns the value of attribute metric.



56
57
58
# File 'lib/semantic_logger/log.rb', line 56

def metric
  @metric
end

#metric_amountObject

Returns the value of attribute metric_amount.



56
57
58
# File 'lib/semantic_logger/log.rb', line 56

def metric_amount
  @metric_amount
end

#nameObject

Returns the value of attribute name.



56
57
58
# File 'lib/semantic_logger/log.rb', line 56

def name
  @name
end

#named_tagsObject

Returns the value of attribute named_tags.



56
57
58
# File 'lib/semantic_logger/log.rb', line 56

def named_tags
  @named_tags
end

#payloadObject

Returns the value of attribute payload.



56
57
58
# File 'lib/semantic_logger/log.rb', line 56

def payload
  @payload
end

#tagsObject

Returns the value of attribute tags.



56
57
58
# File 'lib/semantic_logger/log.rb', line 56

def tags
  @tags
end

#thread_nameObject

Returns the value of attribute thread_name.



56
57
58
# File 'lib/semantic_logger/log.rb', line 56

def thread_name
  @thread_name
end

#timeObject

Returns the value of attribute time.



56
57
58
# File 'lib/semantic_logger/log.rb', line 56

def time
  @time
end

Instance Method Details

#assign(message: nil, payload: nil, min_duration: 0.0, exception: nil, metric: nil, metric_amount: nil, duration: nil, backtrace: nil, log_exception: :full, on_exception_level: nil, dimensions: nil) ⇒ Object

Assign named arguments to this log entry, supplying defaults where applicable

Returns [true|false] whether this log entry should be logged

Example:

logger.info(name: 'value')


77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
# File 'lib/semantic_logger/log.rb', line 77

def assign(message: nil,
           payload: nil,
           min_duration: 0.0,
           exception: nil,
           metric: nil,
           metric_amount: nil,
           duration: nil,
           backtrace: nil,
           log_exception: :full,
           on_exception_level: nil,
           dimensions: nil)
  self.message       = message
  self.payload       = payload
  self.duration      = duration
  self.metric        = metric
  self.metric_amount = metric_amount
  self.dimensions    = dimensions

  if exception
    unless exception.is_a?(Exception)
      exception = ArgumentError.new("Invalid value for logger exception: #{exception.inspect}")
      exception.set_backtrace(Utils.extract_backtrace(caller))
    end

    case log_exception
    when :full
      self.exception = exception
    when :partial
      self.message = "#{message} -- Exception: #{exception.class}: #{exception.message}"
    when nil, :none
      # Log the message without the exception that was raised
      nil
    else
      raise(ArgumentError, "Invalid value:#{log_exception.inspect} for argument :log_exception")
    end
    # On exception change the log level
    if on_exception_level
      self.level       = on_exception_level
      self.level_index = Levels.index(level)
    end
  end

  # Elastic logging: Log when :duration exceeds :min_duration
  # Except if there is an exception when it will always be logged
  return false if duration && (duration < min_duration) && exception.nil?

  if backtrace
    self.backtrace = Utils.extract_backtrace(backtrace)
  elsif level_index >= SemanticLogger.backtrace_level_index
    self.backtrace = Utils.extract_backtrace(caller)
  end

  true
end

#assign_hash(hash) ⇒ Object

Assign known keys to self, all other keys to the payload.



139
140
141
142
143
144
145
146
147
148
149
150
# File 'lib/semantic_logger/log.rb', line 139

def assign_hash(hash)
  self.payload ||= {}
  hash.each_pair do |key, value|
    if ASSIGNABLE_KEYS.include?(key) && respond_to?(:"#{key}=")
      public_send(:"#{key}=", value)
    else
      payload[key] = value
    end
  end
  self.payload = nil if payload.empty?
  self
end

#backtrace_to_sObject

Returns [String] the exception backtrace including all of the child / caused by exceptions



201
202
203
204
205
206
207
208
209
210
211
# File 'lib/semantic_logger/log.rb', line 201

def backtrace_to_s
  trace = ""
  each_exception do |exception, i|
    if i.zero?
      trace = (exception.backtrace || []).join("\n")
    else
      trace << "\nCause: #{exception.class.name}: #{exception.message}\n#{(exception.backtrace || []).join("\n")}"
    end
  end
  trace
end

#cleansed_messageObject

Strip the standard Rails colorizing from the logged message.

Note: This unconditionally strips ANSI colorization, and is used to keep terminal escape codes out of structured (JSON/Loki) output. It is distinct from Formatters::Base#escape_control_characters, which instead escapes (preserves) control characters and is opt-in for the text formatters.



284
285
286
287
288
289
# File 'lib/semantic_logger/log.rb', line 284

def cleansed_message
  msg = message.to_s
  return msg.strip unless msg.include?("\e")

  msg.gsub(/\e\[[\d;]*[mz]?|\e/, "").strip
end

#duration_humanObject

Returns [String] the duration in human readable form



229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
# File 'lib/semantic_logger/log.rb', line 229

def duration_human
  return nil unless duration

  seconds = duration / 1000
  if seconds >= 86_400.0 # 1 day
    "#{(seconds / 86_400).to_i}d #{Time.at(seconds).utc.strftime('%-Hh %-Mm')}"
  elsif seconds >= 3600.0 # 1 hour
    Time.at(seconds).utc.strftime("%-Hh %-Mm")
  elsif seconds >= 60.0 # 1 minute
    Time.at(seconds).utc.strftime("%-Mm %-Ss")
  elsif seconds >= 1.0 # 1 second
    "#{format('%.3f', seconds)}s"
  else
    duration_to_s
  end
end

#duration_to_sObject



217
218
219
# File 'lib/semantic_logger/log.rb', line 217

def duration_to_s
  "#{duration.to_i}ms" if duration
end

#each_exceptionObject

Call the block for exception and any nested exception



179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
# File 'lib/semantic_logger/log.rb', line 179

def each_exception
  # With thanks to https://github.com/bugsnag/bugsnag-ruby/blob/6348306e44323eee347896843d16c690cd7c4362/lib/bugsnag/notification.rb#L81
  depth      = 0
  exceptions = []
  ex         = exception
  while !ex.nil? && !exceptions.include?(ex) && exceptions.length < MAX_EXCEPTIONS_TO_UNWRAP
    exceptions << ex
    yield(ex, depth)

    depth += 1
    ex    =
      if ex.respond_to?(:cause) && ex.cause
        ex.cause
      elsif ex.respond_to?(:continued_exception) && ex.continued_exception
        ex.continued_exception
      elsif ex.respond_to?(:original_exception) && ex.original_exception
        ex.original_exception
      end
  end
end

#extract_arguments(payload, message = nil) ⇒ Object

Extract the arguments from a Hash Payload

Raises:

  • (ArgumentError)


153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
# File 'lib/semantic_logger/log.rb', line 153

def extract_arguments(payload, message = nil)
  raise(ArgumentError, "payload must be a Hash") unless payload.is_a?(Hash)

  message = nil if message == ""
  if payload.key?(:payload)
    return message ? payload.merge(message: message) : payload
  end

  new_payload = {}
  args        = {}
  payload.each_pair do |key, value|
    # Supplied message takes precedence
    if (key == :message) && !message.nil?
      new_payload[key] = value
      next
    end

    NON_PAYLOAD_KEYS.include?(key) ? args[key] = value : new_payload[key] = value
  end
  args[:payload] = new_payload unless new_payload.empty?
  args[:message] = message if message
  args
end

#extract_file_and_line(stack, short_name = false) ⇒ Object

Extract the filename and line number from the last entry in the supplied backtrace



262
263
264
265
266
267
268
269
# File 'lib/semantic_logger/log.rb', line 262

def extract_file_and_line(stack, short_name = false)
  return unless stack&.size&.positive?

  match = CALLER_REGEXP.match(stack.first)
  return unless match

  [short_name ? File.basename(match[1]) : match[1], match[2].to_i]
end

#file_name_and_line(short_name = false) ⇒ Object

Returns [String, String] the file_name and line_number from the backtrace supplied in either the backtrace or exception



273
274
275
276
# File 'lib/semantic_logger/log.rb', line 273

def file_name_and_line(short_name = false)
  stack = backtrace || exception&.backtrace
  extract_file_and_line(stack, short_name)
end

#level_to_sObject

Returns [String] single character upper case log level



247
248
249
# File 'lib/semantic_logger/log.rb', line 247

def level_to_s
  level.to_s[0..0].upcase
end

#metric_only?Boolean

A metric only event has a metric but no message or exception.

Returns:

  • (Boolean)


314
315
316
# File 'lib/semantic_logger/log.rb', line 314

def metric_only?
  metric && message.nil? && exception.nil?
end

#payload?Boolean

Returns [true|false] whether the log entry has a payload

Returns:

  • (Boolean)


298
299
300
# File 'lib/semantic_logger/log.rb', line 298

def payload?
  !(payload.nil? || (payload.respond_to?(:empty?) && payload.empty?))
end

#payload_to_sObject

Return the payload in text form Returns nil if payload is missing or empty



293
294
295
# File 'lib/semantic_logger/log.rb', line 293

def payload_to_s
  payload.inspect if payload?
end

#process_info(thread_name_length = 30) ⇒ Object

DEPRECATED



252
253
254
255
256
257
# File 'lib/semantic_logger/log.rb', line 252

def process_info(thread_name_length = 30)
  file, line = file_name_and_line(true)
  file_name  = " #{file}:#{line}" if file

  "#{$$}:#{format("%.#{thread_name_length}s", thread_name)}#{file_name}"
end

#set_context(key, value) ⇒ Object

Lazy initializes the context hash and assigns a key value pair.



309
310
311
# File 'lib/semantic_logger/log.rb', line 309

def set_context(key, value)
  (self.context ||= {})[key] = value
end

#to_h(host = SemanticLogger.host, application = SemanticLogger.application, environment = SemanticLogger.environment) ⇒ Object



302
303
304
305
306
# File 'lib/semantic_logger/log.rb', line 302

def to_h(host = SemanticLogger.host, application = SemanticLogger.application,
         environment = SemanticLogger.environment)
  logger = Struct.new(:host, :application, :environment).new(host, application, environment)
  SemanticLogger::Formatters::Raw.new.call(self, logger)
end