module Sapience

Stream appender

Writes log messages to a file or open IO stream

Send log messages to any standard Ruby logging class.

Forwards logging call to loggers such as Logger, log4r, etc.

Send log messages to sentry

Example:

Sapience.add_appender(:stream, {io: STDOUT, formatter: :color})

rubocop:disable ClassVars, Style/SafeNavigation

Sapience::Loggers::Concurrent is a class wrapping all methods necessary for integration with concurrent-ruby gem .

Example:

Sapience.add_appender(:datadog, {url: "udp://localhost:2222"})

rubocop:disable ClassVars

Abstract Subscriber

Abstract base class for appender and metrics subscribers.

Constants

APPENDER_NAMESPACE
APP_NAME

Logging levels in order of most detailed to most severe

AppNameMissing
DEFAULT_ENV
DEFAULT_STATSD_URL
ERROR_HANDLER_NAMESPACE
InvalidLogExecutor
LEVELS
Log

rubocop:disable LineLength

METRICS_NAMESPACE
MissingConfiguration
RACK_ENV
RAILS_ENV
SAPIENCE_ENV
TestException
UnknownClass
UnkownLogLevel
VERSION

Public Class Methods

[](klass) click to toggle source

Return a logger for the supplied class or class_name

# File lib/sapience/sapience.rb, line 131
def self.[](klass)
  Sapience::Logger.new(klass)
end
add_appender(appender_class_name, options = {}, _deprecated_level = nil, &_block) click to toggle source

Add a new logging appender as a new destination for all log messages emitted from Sapience

Appenders will be written to in the order that they are added

If a block is supplied then it will be used to customize the format of the messages sent to that appender. See Sapience::Logger.new for more information on custom formatters

Parameters

file_name: [String]
  File name to write log messages to.

Or,
io: [IO]
  An IO Stream to log to.
  For example STDOUT, STDERR, etc.

Or,
appender: [Symbol|Sapience::Subscriber]
  A symbol identifying the appender to create.
  For example:
    :bugsnag, :elasticsearch, :graylog, :http, :mongodb, :new_relic, :splunk_http, :syslog, :wrapper
       Or,
  An instance of an appender derived from Sapience::Subscriber
  For example:
    Sapience::Appender::Http.new(url: 'http://localhost:8088/path')

Or,
logger: [Logger|Log4r]
  An instance of a Logger or a Log4r logger.

level: [:trace | :debug | :info | :warn | :error | :fatal]
  Override the log level for this appender.
  Default: Sapience.config.default_level

formatter: [Symbol|Object|Proc]
  Any of the following symbol values: :default, :color, :json
    Or,
  An instance of a class that implements #call
    Or,
  A Proc to be used to format the output from this appender
  Default: :default

filter: [Regexp|Proc]
  RegExp: Only include log messages where the class name matches the supplied.
  regular expression. All other messages will be ignored.
  Proc: Only include log messages where the supplied Proc returns true
        The Proc must return true or false.

Examples:

# Send all logging output to Standard Out (Screen)
Sapience.add_appender(:stream, io: STDOUT)

# Send all logging output to a file
Sapience.add_appender(:stream, file_name: 'logfile.log')

# Send all logging output to a file and only :info and above to standard output
Sapience.add_appender(:stream, file_name: 'logfile.log')
Sapience.add_appender(:stream, io: STDOUT, level: :info)

Log to log4r, Logger, etc.:

# Send logging output to an existing logger
require 'logger'
require 'sapience'

# Built-in Ruby logger
log = Logger.new(STDOUT)
log.level = Logger::DEBUG

Sapience.config.default_level = :debug
Sapience.add_appender(:wrapper, logger: log)

logger = Sapience['Example']
logger.info "Hello World"
logger.debug("Login time", user: 'Joe', duration: 100, ip_address: '127.0.0.1')
# File lib/sapience/sapience.rb, line 213
def self.add_appender(appender_class_name, options = {}, _deprecated_level = nil, &_block)
  fail ArgumentError, "options should be a hash" unless options.is_a?(Hash)
  options        = options.dup.deep_symbolize_keyz!
  appender_class = constantize_symbol(appender_class_name)
  validate_appender_class!(appender_class)

  appender = appender_class.new(options)
  warn "appender #{appender} with (#{options.inspect}) is not valid" unless appender.valid?
  @@appenders << appender

  # Start appender thread if it is not already running
  Sapience::Logger.start_appender_thread
  Sapience::Logger.start_invalid_appenders_task
  appender
end
add_appenders(*appenders) click to toggle source

Examples:

Sapience.add_appenders(
  { file: { io: STDOUT } },
  { sentry: { dsn: "https://app.getsentry.com/" } },
)
# File lib/sapience/sapience.rb, line 245
def self.add_appenders(*appenders)
  appenders.flatten.compact.each do |appender|
    appender.each do |name, options|
      add_appender(name, options)
    end
  end
end
app_name() click to toggle source
# File lib/sapience/sapience.rb, line 107
def self.app_name
  config.app_name ||= app_name_builder
  fail AppNameMissing, "app_name is not configured. See documentation for more information" unless config.app_name
  config.app_name
end
app_name_builder() click to toggle source
# File lib/sapience/sapience.rb, line 470
def self.app_name_builder
  config_hash.fetch(environment) { {} }["app_name"] ||
    config_hash.fetch(DEFAULT_ENV) { {} }["app_name"] ||
    ENV[APP_NAME]
end
appenders() click to toggle source

Returns [Sapience::Subscriber] a copy of the list of active appenders for debugging etc. Use Sapience.add_appender and Sapience.remove_appender to manipulate the active appenders list

# File lib/sapience/sapience.rb, line 272
def self.appenders
  @@appenders.clone
end
capture_exception(exception, payload = {}) click to toggle source
# File lib/sapience/sapience.rb, line 292
def self.capture_exception(exception, payload = {})
  error_handler.capture_exception(exception, payload)
end
capture_message(message, payload = {}) click to toggle source
# File lib/sapience/sapience.rb, line 296
def self.capture_message(message, payload = {})
  error_handler.capture_message(message, payload)
end
clear_tags!() click to toggle source
# File lib/sapience/sapience.rb, line 388
def self.clear_tags!
  Thread.current[:sapience_tags] = []
end
close() click to toggle source

Close and flush all appenders

# File lib/sapience/sapience.rb, line 337
def self.close
  Sapience::Logger.close
end
config() click to toggle source
# File lib/sapience/sapience.rb, line 59
def self.config
  @@config ||= begin
    options = config_hash[environment]
    options ||= default_options(config_hash)
    Configuration.new(options)
  end
end
config_hash() click to toggle source
# File lib/sapience/sapience.rb, line 55
def self.config_hash
  @@config_hash ||= ConfigLoader.load_from_file
end
configure(force: false) { |config| ... } click to toggle source
# File lib/sapience/sapience.rb, line 45
def self.configure(force: false)
  yield config if block_given?
  return config if configured? && force == false
  reset_appenders!
  add_appenders(*config.appenders)
  @@configured = true

  config
end
configured?() click to toggle source
# File lib/sapience/sapience.rb, line 67
def self.configured?
  @@configured
end
constantize(class_name) click to toggle source
# File lib/sapience/sapience.rb, line 455
def self.constantize(class_name)
  return class_name unless class_name.is_a?(String)
  if RUBY_VERSION.to_i >= 2
    Object.const_get(class_name)
  else
    class_name.split("::").inject(Object) { |o, name| o.const_get(name) }
  end
rescue NameError
  raise UnknownClass, "Could not find class: #{class_name}."
end
constantize_symbol(symbol, namespace = APPENDER_NAMESPACE) click to toggle source
# File lib/sapience/sapience.rb, line 450
def self.constantize_symbol(symbol, namespace = APPENDER_NAMESPACE)
  class_name = "#{namespace}::#{symbol.to_sym.camelize}"
  constantize(class_name)
end
create_class(config_section, namespace) click to toggle source
# File lib/sapience/sapience.rb, line 300
def self.create_class(config_section, namespace)
  namespace_string = namespace.to_s.split("::").last

  fail MissingConfiguration, "No #{namespace_string} configured" unless config_section
  klass_name = config_section.keys.first
  options    = config_section.values.first

  klass = constantize_symbol(klass_name, namespace)

  if namespace.descendants.include?(klass)
    klass.new(options)
  else
    fail NotImplementedError, "Unknown #{namespace_string} '#{klass_name}'"
  end
end
default_options(options = {}) click to toggle source
# File lib/sapience/sapience.rb, line 71
def self.default_options(options = {})
  warn "No configuration for environment #{environment}. Using 'default'" unless environment =~ /default|rspec/
  options[DEFAULT_ENV]
end
environment() click to toggle source
# File lib/sapience/sapience.rb, line 92
def self.environment
  @@environment ||=
    ENV.fetch(SAPIENCE_ENV) do
      ENV.fetch(RAILS_ENV) do
        ENV.fetch(RACK_ENV) do
          if defined?(::Rails) && ::Rails.respond_to?(:env)
            ::Rails.env
          else
            DEFAULT_ENV
          end
        end
      end
    end
end
error_handler() click to toggle source
# File lib/sapience/sapience.rb, line 288
def self.error_handler
  @@error_handler ||= create_class(config.error_handler, ERROR_HANDLER_NAMESPACE)
end
error_handler=(error_handler) click to toggle source
# File lib/sapience/sapience.rb, line 284
def self.error_handler=(error_handler)
  @@error_handler = error_handler
end
fast_tag(tag) { || ... } click to toggle source

If the tag being supplied is definitely a string then this fast tag api can be used for short lived tags

# File lib/sapience/sapience.rb, line 353
def self.fast_tag(tag)
  (Thread.current[:sapience_tags] ||= []) << tag
  yield
ensure
  Thread.current[:sapience_tags].pop
end
flush() click to toggle source

Wait until all queued log messages have been written and flush all active appenders

# File lib/sapience/sapience.rb, line 332
def self.flush
  Sapience::Logger.flush
end
known_appenders() click to toggle source
# File lib/sapience/sapience.rb, line 236
def self.known_appenders
  @known_appenders ||= Sapience::Subscriber.descendants
end
log_executor_class() click to toggle source
# File lib/sapience/sapience.rb, line 446
def self.log_executor_class
  constantize_symbol(config.log_executor, "Concurrent")
end
logger() click to toggle source
# File lib/sapience/sapience.rb, line 320
def self.logger
  @@logger ||= Sapience::Logger.logger
end
logger=(logger) click to toggle source
# File lib/sapience/sapience.rb, line 316
def self.logger=(logger)
  @@logger = Sapience::Logger.logger = logger
end
metrics() click to toggle source
# File lib/sapience/sapience.rb, line 280
def self.metrics
  @@metrics ||= create_class(config.metrics, METRICS_NAMESPACE)
end
metrics=(metrics) click to toggle source
# File lib/sapience/sapience.rb, line 276
def self.metrics=(metrics)
  @@metrics = metrics
end
namify(appname, sep = "_") click to toggle source
# File lib/sapience/sapience.rb, line 113
def self.namify(appname, sep = "_")
  return unless appname.is_a?(String)
  return if appname.empty?

  # Turn unwanted chars into the separator
  appname = appname.dup
  appname.gsub!(/[^a-z0-9\-_]+/i, sep)
  unless sep.nil? || sep.empty?
    re_sep = Regexp.escape(sep)
    # No more than one of the separator in a row.
    appname.gsub!(/#{re_sep}{2,}/, sep)
    # Remove leading/trailing separator.
    appname.gsub!(/^#{re_sep}|#{re_sep}$/, "")
  end
  appname.downcase
end
pop_tags(quantity = 1) click to toggle source

Remove specified number of tags from the current tag list

# File lib/sapience/sapience.rb, line 393
def self.pop_tags(quantity = 1)
  t = Thread.current[:sapience_tags]
  return if t.nil?

  t.pop(quantity)
end
push_tags(*tags) click to toggle source

Add tags to the current scope Returns the list of tags pushed after flattening them out and removing blanks

# File lib/sapience/sapience.rb, line 380
def self.push_tags(*tags)
  # Need to flatten and reject empties to support calls from Rails 4
  new_tags                       = tags.flatten.collect(&:to_s).reject(&:empty?)
  t                              = Thread.current[:sapience_tags]
  Thread.current[:sapience_tags] = t.nil? ? new_tags : t.concat(new_tags)
  new_tags
end
remove_appender(appender) click to toggle source

Remove an existing appender Currently only supports appender instances TODO: Make it possible to remove appenders by type Maybe create a concurrent collection that allows this by inheriting from concurrent array.

# File lib/sapience/sapience.rb, line 257
def self.remove_appender(appender)
  @@appenders.delete(appender)
end
remove_appenders(appenders = @@appenders) click to toggle source

Remove specific appenders or all existing

# File lib/sapience/sapience.rb, line 262
def self.remove_appenders(appenders = @@appenders)
  appenders.each do |appender|
    remove_appender(appender)
  end
end
reopen() click to toggle source

After forking an active process call Sapience.reopen to re-open any open file handles etc to resources

Note: Only appenders that implement the reopen method will be called

# File lib/sapience/sapience.rb, line 345
def self.reopen
  @@appenders.each { |appender| appender.reopen if appender.respond_to?(:reopen) }
  # After a fork the appender thread is not running, start it if it is not running
  Sapience::Logger.start_appender_thread
end
reset!() click to toggle source
# File lib/sapience/sapience.rb, line 76
def self.reset!
  @@config        = nil
  @@logger        = nil
  @@metrics       = nil
  @@error_handler = nil
  @@environment   = nil
  @@configured    = false
  @@config_hash   = nil
  clear_tags!
  reset_appenders!
end
reset_appenders!() click to toggle source
# File lib/sapience/sapience.rb, line 88
def self.reset_appenders!
  @@appenders = Concurrent::Array.new
end
root() click to toggle source
# File lib/sapience/sapience.rb, line 466
def self.root
  @root ||= Gem::Specification.find_by_name("sapience").gem_dir
end
silence(new_level = :error) { || ... } click to toggle source

Silence noisy log levels by changing the default_level within the block

This setting is thread-safe and only applies to the current thread

Any threads spawned within the block will not be affected by this setting

silence can be used to both raise and lower the log level within the supplied block.

Example:

# Perform trace level logging within the block when the default is higher
Sapience.config.default_level = :info

logger.debug 'this will _not_ be logged'

Sapience.silence(:trace) do
  logger.debug "this will be logged"
end

Parameters

new_level
  The new log level to apply within the block
  Default: :error

Example:

# Silence all logging for this thread below :error level
Sapience.silence do
  logger.info "this will _not_ be logged"
  logger.warn "this neither"
  logger.error "but errors will be logged"
end

Note:

#silence does not affect any loggers which have had their log level set
explicitly. I.e. That do not rely on the global default level
# File lib/sapience/sapience.rb, line 436
def self.silence(new_level = :error)
  current_index                     = Thread.current[:sapience_silence]
  Thread.current[:sapience_silence] = Sapience.config.level_to_index(new_level)
  yield
ensure
  Thread.current[:sapience_silence] = current_index
end
tagged(*tags) { |self| ... } click to toggle source

Add the supplied tags to the list of tags to log for this thread whilst the supplied block is active. Returns result of block

# File lib/sapience/sapience.rb, line 363
def self.tagged(*tags)
  new_tags = push_tags(*tags)
  yield self
ensure
  pop_tags(new_tags.size)
end
tags() click to toggle source

Returns a copy of the [Array] of [String] tags currently active for this thread Returns nil if no tags are set

# File lib/sapience/sapience.rb, line 372
def self.tags
  # Since tags are stored on a per thread basis this list is thread-safe
  t = Thread.current[:sapience_tags]
  t.nil? ? [] : t.clone
end
test_exception(_level = :error) click to toggle source
# File lib/sapience/sapience.rb, line 324
def self.test_exception(_level = :error)
  fail Sapience::TestException, "Sapience Test Exception"
rescue Sapience::TestException => ex
  Sapience.capture_exception(ex,  test_exception: true)
end
validate_appender_class!(appender_class) click to toggle source
# File lib/sapience/sapience.rb, line 229
def self.validate_appender_class!(appender_class)
  return if known_appenders.include?(appender_class)

  fail NotImplementedError,
    "Unknown appender '#{appender_class}'. Supported appenders are (#{known_appenders.join(", ")})"
end