DebugMe
|
## A Classic Debugging Technique That Never Gets Old Printing labeled variable values to STDOUT is one of the oldest and most fundamental debugging techniques in computer science. From the earliest days of programming, developers have relied on simple output statements to understand what their code is doing. While modern debuggers offer sophisticated features like breakpoints and step-through execution, there's something elegantly simple and universally effective about printing variables to see their values in real-time. **DebugMe** embraces this time-tested approach, making it effortless to inspect local, instance, and class variables with clearly labeled output. Sometimes the old ways are the best ways. |
Quick Start
gem install debug_me
require 'debug_me'
include DebugMe
my_variable = 42
debug_me { :my_variable } # Use a Symbol for the variable name
# or if you have several variables you want to see ...
debug_me { i[ var1 var2 var3 var4 ]}
DebugMe::debug_me(){} works with local, instance and class variables.
Installation
Add this line to your application's Gemfile:
bundle add 'debug_me'
Or install it yourself:
gem install debug_me
Examples Usage
require 'debug_me'
include DebugMe
debug_me # Prints only the header banner consisting of tag, method name, file name and line number
debug_me('INFO') # Also prints only the header but with a different tag
debug_me(levels: 5) # Along with the header, show the call stack back this many levels
debug_me{:backtrace} # show the entire call stack
debug_me{[ :backtrace ]} will # show the entire call stack
debug_me(levels: 5){[ :backtrace ]} # will only show the first 5 levels of the backtrace - ie. the levels parameter supersedes :backtrace
debug_me {} # prints the default header and __ALL__ variables
debug_me {:just_this_variable} # prints the default header and the value of only one specific variable
debug_me { i[ this_one that_one that_other_one ]} # prints default header and three specific variables
# Use an array of symbols and strings to pass multiple variables for output
# Each element of the array is 'eval'ed with the context binding of the caller
debug_me(){[ :my_var, 'my_complex_var_or_method[my_var]' ]}
debug_me(header: false) {} # disables the printing of the header; prints all variables
debug_me(tag: 'MyTag', :header => false) {} # disables header, sets different tag, prints all variables
debug_me('=== LOOK ===') {} # changes the tag and prints all variables with a header line
debug_me('=== LOOK ===') {:@foo} # changes the tag, prints a header line and a specific instance variable
debug_me('=== LOOK ===') {:@@foo} # changes the tag, prints a header line and a specific class variable
debug_me(ivar: false, cvar: false) {} # print only the local variables with the default tag and a header line
Most of the examples above use symbols to designate the variables that you want to be shown with their name as a label. You can also use strings. With strings you are not limited to just variables. Consider these examples:
debug_me {[ 'some_array.size', 'SomeDatabaseModel.count' ]}
# What a backtrace with your variables?
debug_me {[
:my_variable,
'some_hash.keys.reject{|k| k.to_s.start_with?('A')}',
'caller' ]} # yes, caller is a kernel method that will give a backtrace
# You can also get into trouble so be careful. The symbols and strings
# are evaluated in the context of the caller. Within the string any
# command or line of code can be given. SO DO NOT try to use
# something silly like debug_me{ 'system("rm -fr /")'}
Security Warning
IMPORTANT: debug_me uses eval() to evaluate the symbols and strings you pass to it in the context of the caller. This provides powerful flexibility but comes with significant security implications:
- Never pass untrusted input to
debug_me - Never use
debug_mewith user-supplied data or parameters - Only use
debug_meduring development and testing - Remove or disable
debug_mecalls in production code (use$DEBUG_ME = false)
The ability to execute arbitrary code is by design for debugging flexibility, but it means any string passed to debug_me will be evaluated as Ruby code. Treat this tool with the same caution you would any eval() statement.
For production environments, consider using a proper logging framework with structured logging instead of debug_me.
Disabling Debug Output with $DEBUG_ME
The $DEBUG_ME global variable provides a convenient way to enable or disable all debug_me output throughout your application without removing the code. This is particularly useful for QA environments.
Environment Variable Control (Recommended)
The DEBUG_ME environment variable is automatically detected when the gem is loaded. This is the recommended approach for controlling debug output across environments without code changes.
# Disable debug output (QA)
export DEBUG_ME=false
# Enable debug output (development)
export DEBUG_ME=true
# Or set inline when running your app
DEBUG_ME=false bundle exec rails server
Supported values:
- Truthy (enables output):
true,yes,1,on, or any other value - Falsy (disables output):
false,no,0,off, or empty string - Not set: Defaults to
true(enabled)
Runtime Control
You can also control $DEBUG_ME programmatically at runtime:
# Enable debug output (default)
$DEBUG_ME = true
# Disable all debug_me output
$DEBUG_ME = false
When $DEBUG_ME is set to false:
- All
debug_mecalls returnnilimmediately - No output is produced (even with custom file or logger options)
- The block is not evaluated, so there's zero performance overhead
Production Setup Examples
Docker/Container deployment:
# Dockerfile
ENV DEBUG_ME=false
Systemd service:
[Service]
Environment="DEBUG_ME=false"
Heroku:
heroku config:set DEBUG_ME=false
For Rails with fallback (only if ENV not set):
# config/initializers/debug_me.rb
require 'debug_me'
# Override only if not already set by environment
$DEBUG_ME = Rails.env.development? || Rails.env.test? unless ENV.key?('DEBUG_ME')
Benefits
- Leave debug code in place: No need to remove
debug_mecalls before deployment - Zero overhead: When disabled, blocks aren't evaluated, so no performance impact
- Easy toggling: Can enable debugging in production temporarily if needed
- Security: Prevents accidental debug output exposure in production
Example
# Your code with debug_me calls
def process_data(items)
debug_me { :items } # Only outputs in development/test
result = items.map do |item|
debug_me { :item } # Only outputs in development/test
transform(item)
end
debug_me { :result } # Only outputs in development/test
result
end
# In production with $DEBUG_ME = false:
# - No output produced
# - No performance overhead
# - Code works normally
Default Options
The default options is a global constant DebugMeDefaultOptions that is outside of the DebugMe name space. I did that so that if you do include DebugMe to make access to the method easier you could still have the constant with a function specific name that would be outside of anything that you may have already coded in you program.
Notice that this constant is outside of the DebugMe's module namespace.
DebugMeDefaultOptions = {
tag: 'DEBUG', # A tag to prepend to each output line
time: true, # Include a time-stamp in front of the tag
strftime: '%Y-%m-%d %H:%M:%S.%6N', # timestamp format
header: true, # Print a header string before printing the variables
levels: 0, # Number of additional backtrack entries to display
skip1: false, # skip 1 blank line in front of a new output block
skip2: false, # skip 2 blank lines in front of a new output block
lvar: true, # Include local variables
ivar: true, # Include instance variables in the output
cvar: true, # Include class variables in the output
cconst: true, # Include class constants
logger: nil, # Pass in a logger class instance like Rails.logger
file: $stdout # The output file
}
If you want the output of the method to always go to STDERR then do this:
require 'debug_me'
DebugMeDefaultOptions[:file] = $stderr # or STDERR
If you want the debug_me output to go to a real file:
DebugMeDefaultOptions[:file] = File.open('debug_me.log', 'w')
Using a Logger class instance
If you are working in Rails and want all the debug_me output to go to the Rails.logger its as easy as:
DebugMeDefaultOptions[:logger] = Rails.logger
Or while working in rails you only want to add a marker to the Rails.logger do this:
debug_me(logger: Rails.logger, tag: 'Hello World')
If you are working in Rails and want to use both the standard debug_me functions and put stuff into the Rails.logger but do not want to always remember the option settings then do something line this in a config/initializers/aaaaa_debug_me.rb file:
# config/initializers/aaaaa_debug_me.rb
require 'debug_me'
module DebugMe
def log_me(, ={}, &block)
= {logger: Rails.logger, time: false, header: false, tag: }
block_given? ? debug_me(, block) : debug_me()
end
end
include DebugMe
# Just setup the base name. The parent path will be added below ...
debug_me_file_name = 'debug_me'
# NOTE: you could add a timestamp to the filename
# debug_me_file_name += '_' + Time.now.strftime('%Y%m%d%H%M%S')
debug_me_file_name += '_' + Process.argv0.gsub('/',' ').split(' ').last
# NOTE: by using the Process.argv0 ... you get multiple log files for
# rails, rake, sidekiq etc.
debug_me_file_name += '.log'
debug_me_filepath = Rails.root + 'log' + debug_me_file_name
debug_me_file = File.open(debug_me_filepath, 'w')
debug_me_file.puts "\n==================== Starting New Test Run --------->>>>>>\n\n"
# Set application wide options in the DebugMeDefaultOptions hash
DebugMeDefaultOptions[:file] = debug_me_file
debug_me{['ENV']}
What that does for your Rails application is dump all of your system environment variables and their values to a debug_me log file in the log directory of the application.
It also adds a new method log_me which you can use to send stuff to the Rails.logger instance. The method used by debug_me for the logger instance is always the debug method.
Conclusion
The rest of the default options are obvious.
You can always over-ride the default options on a case by case basis like this:
debug_me {...}
# ...
debug_me(header: false){...}
Contributing
- Fork it ( https://github.com/[my-github-username]/debug_me/fork )
- Create your feature branch (
git checkout -b my-new-feature) - Commit your changes (
git commit -am 'Add some feature') - Push to the branch (
git push origin my-new-feature) - Create a new Pull Request