Class: Aspera::Log

Inherits:
Object
  • Object
show all
Includes:
Singleton
Defined in:
lib/aspera/log.rb

Overview

Singleton object for logging

Constant Summary collapse

LOG_TYPES =

Where logs are sent to

%i[stderr stdout syslog].freeze
LEVELS =

Levels are :trace2,:trace1,:debug,:info,:warn,:error,fatal,:unknown

Logger::Severity.constants.sort{ |a, b| Logger::Severity.const_get(a) <=> Logger::Severity.const_get(b)}.map{ |c| c.downcase.to_sym}.freeze

Instance Attribute Summary collapse

Class Method Summary collapse

Instance Method Summary collapse

Instance Attribute Details

#dump_formatObject

Returns the value of attribute dump_format.



143
144
145
# File 'lib/aspera/log.rb', line 143

def dump_format
  @dump_format
end

#loggerObject (readonly)

Returns the value of attribute logger.



142
143
144
# File 'lib/aspera/log.rb', line 142

def logger
  @logger
end

#logger_typeObject

Returns the value of attribute logger_type.



142
143
144
# File 'lib/aspera/log.rb', line 142

def logger_type
  @logger_type
end

Class Method Details

.caller_methodObject

Returns the last 2 containers (module/class) and method caller



117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
# File 'lib/aspera/log.rb', line 117

def caller_method
  stack = caller
  i = stack.rindex{ |line| line.include?('Logger')}
  frame = stack[i + 1] if i && stack[i + 1]
  return '???' unless frame
  # Extract the "Class::Module::Method" or "Class#method" part
  full = frame[/'([^']+)'/, 1]
  return '???' unless full
  # Split into class/module and method parts
  parts = full.split(/(::|#)/)
  # Reconstruct keeping only last two class/module names + separator + method
  if parts.include?('#')
    sep_index = parts.index('#')
    classes = parts[0...sep_index].join
    method = parts[sep_index + 1]
  else
    classes = parts[0..-2].join
    method = parts.last
  end
  class_parts = classes.split('::')
  selected_classes = class_parts.last(2).join('::')
  "#{selected_classes}.#{method}"
end

.capture_stderrObject

Capture the output of $stderr and log it at debug level

Yield Returns:

  • (void)

    Code block whose stderr output will be captured



107
108
109
110
111
112
113
114
# File 'lib/aspera/log.rb', line 107

def capture_stderr
  real_stderr = $stderr
  $stderr = StringIO.new
  yield if block_given?
  log.debug($stderr.string)
ensure
  $stderr = real_stderr
end

.dump(name, object = nil, level: :debug, &block) ⇒ Object

Dump object (Hash) to log using specified level

Parameters:

  • name (String, Symbol)

    Name of object dumped

  • object (Hash, nil) (defaults to: nil)

    Data to dump

  • level (Symbol) (defaults to: :debug)

    Debug level

Yield Returns:

  • (Object)

    Computed object to dump (alternative to object parameter)



85
86
87
88
89
90
# File 'lib/aspera/log.rb', line 85

def dump(name, object = nil, level: :debug, &block)
  return unless instance.logger.send(:"#{level}?")
  Aspera.assert(object.nil? || block.nil?, 'Use either object, or block, not both')
  object = yield if block_given?
  instance.logger.send(level, obj_dump(name, object))
end

.instanceLog

Returns the singleton instance of Log

Returns:

  • (Log)

    the singleton instance



61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
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
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
# File 'lib/aspera/log.rb', line 61

class Log
  include Singleton

  # Where logs are sent to
  LOG_TYPES = %i[stderr stdout syslog].freeze

  # Levels are :trace2,:trace1,:debug,:info,:warn,:error,fatal,:unknown
  LEVELS = Logger::Severity.constants.sort{ |a, b| Logger::Severity.const_get(a) <=> Logger::Severity.const_get(b)}.map{ |c| c.downcase.to_sym}.freeze

  # Class methods
  class << self
    def short_levl(level)
      "#{level[0, 3]}#{level[-1]}"
    end

    # Get the logger object of singleton
    def log; instance.logger; end

    # Dump object (`Hash`) to log using specified level
    #
    # @param name   [String, Symbol]  Name of object dumped
    # @param object [Hash, nil]       Data to dump
    # @param level  [Symbol]          Debug level
    # @yieldreturn [Object] Computed object to dump (alternative to object parameter)
    def dump(name, object = nil, level: :debug, &block)
      return unless instance.logger.send(:"#{level}?")
      Aspera.assert(object.nil? || block.nil?, 'Use either object, or block, not both')
      object = yield if block_given?
      instance.logger.send(level, obj_dump(name, object))
    end

    # @return [String] Dump of object
    def obj_dump(name, object)
      dump_text =
        case instance.dump_format
        when :json
          JSON.pretty_generate(object) rescue PP.pp(object, +'')
        when :ruby
          PP.pp(object, +'')
        else Aspera.error_unexpected_value(instance.dump_format){'dump format'}
        end
      "#{name.to_s.green}(#{instance.dump_format})#{object.class}=\n#{dump_text}"
    end

    # Capture the output of $stderr and log it at debug level
    # @yieldreturn [void] Code block whose stderr output will be captured
    def capture_stderr
      real_stderr = $stderr
      $stderr = StringIO.new
      yield if block_given?
      log.debug($stderr.string)
    ensure
      $stderr = real_stderr
    end

    # Returns the last 2 containers (module/class) and method caller
    def caller_method
      stack = caller
      i = stack.rindex{ |line| line.include?('Logger')}
      frame = stack[i + 1] if i && stack[i + 1]
      return '???' unless frame
      # Extract the "Class::Module::Method" or "Class#method" part
      full = frame[/'([^']+)'/, 1]
      return '???' unless full
      # Split into class/module and method parts
      parts = full.split(/(::|#)/)
      # Reconstruct keeping only last two class/module names + separator + method
      if parts.include?('#')
        sep_index = parts.index('#')
        classes = parts[0...sep_index].join
        method = parts[sep_index + 1]
      else
        classes = parts[0..-2].join
        method = parts.last
      end
      class_parts = classes.split('::')
      selected_classes = class_parts.last(2).join('::')
      "#{selected_classes}.#{method}"
    end
  end

  attr_reader :logger_type, :logger
  attr_accessor :dump_format

  def program_name=(value)
    @program_name = value
    self.logger_type = @logger_type
  end

  # Set log level of underlying logger given symbol level
  # @param new_level [Symbol] One of LEVELS
  def level=(new_level)
    Aspera.assert_values(new_level, LEVELS)
    @logger.level = Logger::Severity.const_get(new_level.to_sym.upcase)
  end

  def formatter=(formatter)
    if formatter.is_a?(String)
      raise Error, "Unknown formatter #{formatter}, use one of: #{FORMATTERS.join(', ')}" unless FORMATTER_LAMBDAS.key?(formatter.to_sym)
      formatter = FORMATTER_LAMBDAS[formatter.to_sym]
    elsif !formatter.respond_to?(:call) && !formatter.is_a?(Logger::Formatter)
      raise Error, 'Formatter must be a String, a Logger::Formatter or a Proc'
    end
    # Update formatter with password hiding
    @logger.formatter = SecretHider.instance.log_formatter(formatter)
  end

  def formatter
    @logger.formatter
  end

  # Get symbol of debug level of underlying logger
  # @return [Symbol] One of LEVELS
  def level
    Aspera.assert(Logger::SEVERITY_LABEL.key?(@logger.level), 'unexpected logger level value')
    Logger::SEVERITY_LABEL[@logger.level].downcase
  end

  # Change underlying logger, but keep log level (default: INFO)
  def logger_type=(new_log_type)
    # [Integer]
    current_severity_integer = @logger&.level || ENV['AS_LOG_LEVEL']&.to_i || Logger::Severity::INFO
    case new_log_type
    when :stderr
      @logger = Logger.new($stderr, progname: @program_name, formatter: DEFAULT_FORMATTER)
    when :stdout
      @logger = Logger.new($stdout, progname: @program_name, formatter: DEFAULT_FORMATTER)
    when :syslog
      require 'syslog/logger'
      # The syslog class automatically creates methods from the severity names.
      # We just need to add the mapping (but syslog lowest is DEBUG)
      1.upto(Logger::TRACE_MAX).each do |level|
        Syslog::Logger.const_get(:LEVEL_MAP)[Logger.const_get("TRACE#{level}")] = Syslog::LOG_DEBUG
      end
      Logger::Severity.constants.each do |severity|
        Syslog::Logger.make_methods(severity.downcase)
      end
      # Use `local2` facility, like other Aspera components
      @logger = Syslog::Logger.new(@program_name, Syslog::LOG_LOCAL2)
    else Aspera.error_unexpected_value(new_log_type){"log type (#{LOG_TYPES.join(', ')})"}
    end
    @logger.level = current_severity_integer
    @logger_type = new_log_type
    # add secret hider to default logger
    self.formatter = @logger.formatter
  end

  private

  def initialize
    @logger = nil
    @program_name = 'aspera'
    @dump_format = :json
    @logger_type = :stderr
    # This sets @logger and @logger_type (self needed to call method instead of local var)
    self.logger_type = @logger_type
  end

  # Define decoration of levels
  LVL_DECO = {
    TRACE2:  %i{dim},
    TRACE1:  %i{blue},
    DEBUG:   %i{cyan},
    INFO:    %i{green},
    WARN:    %i{bg_brown black},
    ERROR:   %i{bg_red blink},
    FATAL:   %i{magenta},
    UNKNOWN: %i{blink}
  }.freeze

  # Short (4-letters) levels with color
  LVL_COLOR = LVL_DECO.to_h do |k, v|
    [k, short_levl(k).apply(*v)]
  end.freeze

  DEFAULT_FORMATTER = ->(s, _d, _p, m){"#{LVL_COLOR[s]} #{m}\n"}

  # pre-defined formatters
  FORMATTER_LAMBDAS = {
    standard: Logger::Formatter.new,
    default:  DEFAULT_FORMATTER,
    caller:   ->(s, _d, _p, m){"#{LVL_COLOR[s]} #{Log.caller_method}\n#{m}\n"}
  }.freeze

  FORMATTERS = FORMATTER_LAMBDAS.keys

  private_constant :LVL_DECO, :DEFAULT_FORMATTER, :FORMATTER_LAMBDAS
end

.logObject

Get the logger object of singleton



77
# File 'lib/aspera/log.rb', line 77

def log; instance.logger; end

.obj_dump(name, object) ⇒ String

Returns Dump of object.

Returns:



93
94
95
96
97
98
99
100
101
102
103
# File 'lib/aspera/log.rb', line 93

def obj_dump(name, object)
  dump_text =
    case instance.dump_format
    when :json
      JSON.pretty_generate(object) rescue PP.pp(object, +'')
    when :ruby
      PP.pp(object, +'')
    else Aspera.error_unexpected_value(instance.dump_format){'dump format'}
    end
  "#{name.to_s.green}(#{instance.dump_format})#{object.class}=\n#{dump_text}"
end

.short_levl(level) ⇒ Object



72
73
74
# File 'lib/aspera/log.rb', line 72

def short_levl(level)
  "#{level[0, 3]}#{level[-1]}"
end

Instance Method Details

#formatterObject



168
169
170
# File 'lib/aspera/log.rb', line 168

def formatter
  @logger.formatter
end

#formatter=(formatter) ⇒ Object



157
158
159
160
161
162
163
164
165
166
# File 'lib/aspera/log.rb', line 157

def formatter=(formatter)
  if formatter.is_a?(String)
    raise Error, "Unknown formatter #{formatter}, use one of: #{FORMATTERS.join(', ')}" unless FORMATTER_LAMBDAS.key?(formatter.to_sym)
    formatter = FORMATTER_LAMBDAS[formatter.to_sym]
  elsif !formatter.respond_to?(:call) && !formatter.is_a?(Logger::Formatter)
    raise Error, 'Formatter must be a String, a Logger::Formatter or a Proc'
  end
  # Update formatter with password hiding
  @logger.formatter = SecretHider.instance.log_formatter(formatter)
end

#levelSymbol

Get symbol of debug level of underlying logger

Returns:

  • (Symbol)

    One of LEVELS



174
175
176
177
# File 'lib/aspera/log.rb', line 174

def level
  Aspera.assert(Logger::SEVERITY_LABEL.key?(@logger.level), 'unexpected logger level value')
  Logger::SEVERITY_LABEL[@logger.level].downcase
end

#level=(new_level) ⇒ Object

Set log level of underlying logger given symbol level

Parameters:

  • new_level (Symbol)

    One of LEVELS



152
153
154
155
# File 'lib/aspera/log.rb', line 152

def level=(new_level)
  Aspera.assert_values(new_level, LEVELS)
  @logger.level = Logger::Severity.const_get(new_level.to_sym.upcase)
end

#program_name=(value) ⇒ Object



145
146
147
148
# File 'lib/aspera/log.rb', line 145

def program_name=(value)
  @program_name = value
  self.logger_type = @logger_type
end