Skip to content

Instantly share code, notes, and snippets.

@johncox00
Last active December 26, 2016 13:01
Show Gist options
  • Save johncox00/8eff8f9eb94cc47415217f4c038c0489 to your computer and use it in GitHub Desktop.
Save johncox00/8eff8f9eb94cc47415217f4c038c0489 to your computer and use it in GitHub Desktop.
Forensic logging...This will log just about EVERYTHING in any class in which is included. In a blocking/synchronous framework it will negatively impact performance. Do not use in production unless you have a big problem to diagnose and can't figure out a better way. Even then, don't leave it on for long. There is an async option that uses Resque…
class Application < Rails::Application
#...
config.after_initialize do
method_logger_config = YAML::load(ERB.new(File.read(File.join(Rails.root, 'config/constants/method_logger.yaml'))).result)[Rails.env]
if Rails.env != "test"
Rails.application.eager_load!
if Rails.logger.level <= "Logger::Severity::#{method_logger_config["controller_logging_threshold"].upcase}".constantize
ApplicationController.subclasses.each{|c| c.send(:include, MethodLogger) unless method_logger_config["excluded_controllers"].include?(c.to_s)}
end
if Rails.logger.level <= "Logger::Severity::#{method_logger_config["model_logging_threshold"].upcase}".constantize
ActiveRecord::Base.subclasses.each{|c| c.send(:include, MethodLogger) unless method_logger_config["excluded_models"].include?(c.to_s)}
end
end
end
end
class LoggerJob
@queue = :log
@@method_logger = YAML::load(ERB.new(File.read(File.join(Rails.root, 'config/constants/method_logger.yaml'))).result)[Rails.env]
@log_level = @@method_logger["log_level"].to_sym
def self.perform(message)
Rails.logger.send(@log_level, message)
end
end
module MethodLogger
@@method_logger = YAML::load(ERB.new(File.read(File.join(Rails.root, 'config/constants/method_logger.yaml'))).result)[Rails.env]
@@log_level = @@method_logger["log_level"].to_sym || :debug
@@async = Boolean(@@method_logger["async"]) || false
@@record_method = @@async ? :record_async : :record
@@enabled = Boolean(@@method_logger["enabled"]) || false
# here we exclude methods that will cause circular reference within this module
# or that the logging thereof isn't necessarily useful
@@excluded_class_methods = [:record, :record_async, :format_message, :hidden_actions]
# The hash below defines methods that we know will have passwords in the args.
# The key is the method name. The value is the position within the args where the password is passed.
@@filtered_methods = {
secure_digest: 2
}
@@hostname = Socket.gethostname
def record(timestamp, baseclass, methodname, args, inbound)
string = format_message(timestamp, baseclass, methodname, args, inbound)
Rails.logger.send(@@log_level, string)
end
def record_async(timestamp, baseclass, methodname, args, inbound)
string = format_message(timestamp, baseclass, methodname, args, inbound)
Resque.enqueue(LoggerJob, string)
end
def format_message(timestamp, baseclass, methodname, args, inbound)
"#{@@hostname} #{timestamp.to_formatted_s(:db)} #{inbound ? '<-' : '->'} #{baseclass}##{methodname}(#{args.inspect})"
end
def self.included(base)
if @@enabled
puts "\n\n***** Method Logging: #{base} *****"
# set up the instance methods that we'll override
methods = base.instance_methods(false) + base.private_instance_methods(false)
base.class_eval do
# override the instance methods
methods.each do |method_name|
# get the original method object for use later
original_method = instance_method(method_name)
puts "-> Instance method: #{base}##{method_name}"
define_method(method_name) do |*args, &block|
timestamp = Time.now
# create a new array of args that we can manipulate for logging
new_args = args.dup
# we want to make sure we don't log passwords, so we look for them as a parameter
if original_method.parameters.index([:req, :password])
# find where the password param would be in the args list
pos = original_method.parameters.index([:req, :password])
# find the password arg and filter it out
new_args[pos] = "FILTERED"
end
# find the password arg and filter it out for specific methods
new_args[@@filtered_methods[method_name]] = "FILTERED" if @@filtered_methods.keys.include?(method_name)
# record the message
self.send(@@record_method, timestamp, base, method_name, new_args, true)
# get the return value by calling the original method
return_value = original_method.bind(self).call(*args, &block)
self.send(@@record_method, timestamp, base, method_name, return_value, false)
# return the return from the original method
return_value
end
end
metaclass = class << self
self
end
metaclass.instance_eval do
define_method(:record_async) do |timestamp, baseclass, methodname, args, inbound|
string = format_message(timestamp, baseclass, methodname, args, inbound)
Resque.enqueue(LoggerJob, string)
end
define_method(:record) do |timestamp, baseclass, methodname, args, inbound|
string = format_message(timestamp, baseclass, methodname, args, inbound)
Rails.logger.send(@@log_level, string)
end
define_method(:format_message) do |timestamp, baseclass, methodname, args, inbound|
"#{@@hostname} #{timestamp.to_formatted_s(:db)} #{inbound ? '<-' : '->'} #{baseclass}##{methodname}(#{args.inspect})"
end
(instance_methods - @@excluded_class_methods - ActiveRecord::Base.methods ).each do |method_name|
puts "-> Class method: #{base}##{method_name}"
original_method = base.method(method_name)
define_method(method_name) do |*args, &block|
timestamp = Time.now
new_args = args.dup
if original_method.parameters.index([:req, :password])
pos = original_method.parameters.index([:req, :password])
new_args[pos] = "FILTERED"
end
new_args[@@filtered_methods[method_name]] = "FILTERED" if @@filtered_methods.keys.include?(method_name)
base.send(@@record_method, timestamp, base, method_name, new_args, true)
return_value = original_method.call(*args, &block)
base.send(@@record_method, timestamp, base, method_name, return_value, false)
return_value
end
end
end
end
end
end
end
# Logger levels:
# debug: 0
# info: 1
# warn: 2
# error: 3
# fatal: 4
# unknown: 5
development: &defaults
enabled: false # This is an overall switch to turn method logging on or off.
controller_logging_threshold: debug # This determines at what application logging level we will inject our logging logic into the controllers.
model_logging_threshold: debug # This determines at what application logging level we will inject our logging logic into the models.
log_level: info # Sets the Rails.logger method that will be called with logging messages.
async: false # Determines if we send log messages to file or to Resque/Kafka.
excluded_models: # List models here that do not need granular logging.
- User
excluded_v5_controllers: # List controllers here that do not need granular logging.
- AddressScrubController
test:
<<: *defaults
qa:
<<: *defaults
enabled: false
controller_logging_threshold: info
model_logging_threshold: info
async: false
qa_grn:
<<: *defaults
controller_logging_threshold: info
model_logging_threshold: info
async: false
dev:
<<: *defaults
beta:
<<: *defaults
async: false
production:
<<: *defaults
enabled: false
controller_logging_threshold: info
model_logging_threshold: info
log_level: info
async: false
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment