lib/mongodb_logger/logger.rb
require 'erb'
require 'yaml'
require 'uri'
require 'active_support'
require 'active_support/core_ext'
require 'action_dispatch/http/upload'
require 'mongodb_logger/rails_logger'
require 'mongodb_logger/adapters'
require 'mongodb_logger/replica_set_helper'
module MongodbLogger
class Logger < RailsLogger
include ReplicaSetHelper
DEFAULT_COLLECTION_SIZE = 250.megabytes
# Looks for configuration files in this order
CONFIGURATION_FILES = ["mongodb_logger.yml", "mongoid.yml", "database.yml"]
LOG_LEVEL_SYM = [:debug, :info, :warn, :error, :fatal, :unknown]
ADAPTERS = [
["mongo", Adapers::Mongo],
["moped", Adapers::Moped]
]
attr_reader :db_configuration, :mongo_adapter, :app_root, :app_env
attr_writer :excluded_from_log
def initialize(path = nil, level = DEBUG)
set_root_and_env
begin
path ||= File.join(app_root, "log/#{app_env}.log")
@level = level
internal_initialize
rescue => e
# should use a config block for this
"production" == app_env ? (raise e) : (puts "MongodbLogger WARNING: Using Rails Logger due to exception: #{e.message}")
ensure
if disable_file_logging?
@log = ::Logger.new(STDOUT)
@log.level = @level
else
super(path, @level)
end
end
end
def add_metadata(options = {})
options.each do |key, value|
unless [:messages, :request_time, :ip, :runtime, :application_name, :is_exception, :params, :session, :method].include?(key.to_sym)
Thread.current[:mongodb_logger_mongo_record][key] = value
else
raise ArgumentError, ":#{key} is a reserved key for the mongodb logger. Please choose a different key"
end
end if Thread.current[:mongodb_logger_mongo_record]
end
def add(severity, message = nil, progname = nil, &block)
$stdout.puts(message) if ENV['HEROKU_RACK'] # log in stdout on Heroku
if @level && @level <= severity && (message.present? || progname.present?) && Thread.current[:mongodb_logger_mongo_record].present?
add_log_message(severity, message, progname)
end
# may modify the original message
disable_file_logging? ? message : (@level ? super : message)
end
def mongoize(options = {})
Thread.current[:mongodb_logger_mongo_record] = options.merge({
messages: Hash.new { |hash, key| hash[key] = Array.new },
request_time: Time.now.getutc,
application_name: @db_configuration['application_name']
})
runtime = Benchmark.measure{ yield }.real if block_given?
rescue Exception => e
log_raised_error(e)
# Reraise the exception for anyone else who cares
raise e
ensure
# In case of exception, make sure runtime is set
Thread.current[:mongodb_logger_mongo_record][:runtime] = ((runtime ||= 0) * 1000).ceil
# error callback
Base.on_log_exception(Thread.current[:mongodb_logger_mongo_record]) if Thread.current[:mongodb_logger_mongo_record][:is_exception]
ensure_write_to_mongodb
end
def excluded_from_log
@excluded_from_log ||= nil
end
private
def internal_initialize
configure
connect
check_for_collection
end
def disable_file_logging?
@db_configuration && @db_configuration.fetch(:disable_file_logging, false)
end
def configure
@db_configuration = {
host: 'localhost',
port: 27017,
ssl: false,
capped: true,
capsize: DEFAULT_COLLECTION_SIZE
}.merge(resolve_config).
with_indifferent_access
@db_configuration[:collection] ||= "#{app_env}_log"
@db_configuration[:application_name] ||= resolve_application_name
@db_configuration[:write_options] ||= { w: 0, wtimeout: 200 }
@insert_block = @db_configuration.has_key?(:replica_set) && @db_configuration[:replica_set] ?
lambda { rescue_connection_failure{ insert_log_record(@db_configuration[:write_options]) } } :
lambda { insert_log_record(@db_configuration[:write_options]) }
end
def resolve_application_name
if defined?(Rails)
Rails.application.class.to_s.split("::").first
else
"RackApp"
end
end
def add_log_message(severity, message, progname)
# do not modify the original message used by the buffered logger
msg = (message ? message : progname)
msg = logging_colorized? ? msg.to_s.gsub(/(\e(\[([\d;]*[mz]?))?)?/, '').strip : msg
Thread.current[:mongodb_logger_mongo_record][:messages][LOG_LEVEL_SYM[severity]] << msg
end
def log_raised_error(e)
add(3, "#{e.message}\n#{e.backtrace.join("\n")}")
# log exceptions
Thread.current[:mongodb_logger_mongo_record][:is_exception] = true
end
def ensure_write_to_mongodb
@insert_block.call
rescue
begin
# try to nice serialize record
record_serializer Thread.current[:mongodb_logger_mongo_record], true
@insert_block.call
rescue
# do extra work to inspect (and flatten)
record_serializer Thread.current[:mongodb_logger_mongo_record], false
@insert_block.call rescue nil
end
end
def resolve_config
config = {}
CONFIGURATION_FILES.each do |filename|
config = read_config_from_file(File.join(app_root, 'config', filename))
break unless config.blank?
end
config
end
def read_config_from_file(config_file)
if File.file? config_file
config = ::YAML.load(ERB.new(File.new(config_file).read).result)[app_env]
config = config['mongodb_logger'] if config && config.has_key?('mongodb_logger')
return config unless config.blank?
end
return nil
end
def find_adapter
return Adapers::Mongo if defined?(::Mongo)
return Adapers::Moped if defined?(::Moped)
ADAPTERS.each do |(library, adapter)|
begin
require library
return adapter
rescue LoadError
next
end
end
return nil
end
def connect
adapter = find_adapter
raise "!!! MongodbLogger not found supported adapter. Please, add mongo with bson_ext gems or moped gem into Gemfile !!!" if adapter.nil?
@mongo_adapter ||= adapter.new(@db_configuration)
@db_configuration = @mongo_adapter.configuration
end
def check_for_collection
@mongo_adapter.check_for_collection
end
def insert_log_record(write_options)
return if excluded_from_log && excluded_from_log.any? { |k, v| v.include?(Thread.current[:mongodb_logger_mongo_record][k]) }
@mongo_adapter.insert_log_record(Thread.current[:mongodb_logger_mongo_record], write_options: write_options)
end
def logging_colorized?
# Cache it since these ActiveRecord attributes are assigned after logger initialization occurs in Rails boot
@colorized ||= Object.const_defined?(:ActiveRecord) && ActiveRecord::LogSubscriber.colorize_logging
end
# try to serialyze data by each key and found invalid object
def record_serializer(rec, nice = true)
[:messages, :params].each do |key|
if msgs = rec[key]
msgs.each do |i, j|
msgs[i] = (true == nice ? nice_serialize_object(j) : j.inspect)
end
end
end
end
def nice_serialize_object(data)
case data
when NilClass, String, Fixnum, Bignum, Float, TrueClass, FalseClass, Time, Regexp, Symbol
data
when Hash
hvalues = Hash.new
data.each{ |k,v| hvalues[k] = nice_serialize_object(v) }
hvalues
when Array
data.map{ |v| nice_serialize_object(v) }
when ActionDispatch::Http::UploadedFile, Rack::Test::UploadedFile # uploaded files
{
original_filename: data.original_filename,
content_type: data.content_type
}
else
data.inspect
end
end
def set_root_and_env
if defined? Rails
@app_root, @app_env = Rails.root.to_s, Rails.env.to_s
elsif defined? RACK_ROOT
@app_root, @app_env = RACK_ROOT, (ENV['RACK_ENV'] || 'production')
else
@app_root, @app_env = File.dirname(__FILE__), 'production'
end
end
end
end