Whoop

Gem Version Contribute Maintainability codecov Tests CodeQL StandardRB GEM Version GEM Downloads Ruby Style Twitter Follow

Whoop is a Ruby logging library with built-in formatting and colorization.

To try whoop within your browser, visit the whoop playground.

Installation

Install the gem and add to the application's Gemfile by executing:

$ bundle add whoop

If bundler is not being used to manage dependencies, install the gem by executing:

$ gem install whoop

Configuration

You can configure the gem for you Rails app by adding an an initializer:

# config/initializers/whoop.rb
Whoop.setup do |config|
  config.logger = ActiveSupport::Logger.new("log/debug.log")
  # config.logger = ActiveSupport::Logger.new("log/#{Rails.env}.log")
  # config.logger = ActiveSupport::Logger.new($stdout)
  # config.logger = SemanticLogger["WHOOP"]
  # config.logger = nil # uses `puts`

  config.level = :debug
  # config.level = :info
  # config.level = :warn
  # config.level = :error
end

Usage

The whoop method is accessible from any object. See specs for more examples.

whoop "Hello, World!"

You can pass any options into the whoop method to change the output.

  • label (first argument) - Either the object or title if a block is passed (see below)
  • pattern - String character to use for the line (default is -)
  • count - the number of times to repeat the pattern per line (e.g. 80)
  • color - the color to use for the line (e.g. :red)
  • format - the format to use for the message (one of :json, :sql, :plain, :pretty)
  • caller_depth - the depth of the caller to use for the source (default: 0)
  • explain - whether to run EXPLAIN on the SQL query (default: false)
  • context - a hash of key/value pairs to include in the output
  • tags - an array of tags to include in the output

Examples

whoop "Hello"

# log/debug.log
# ┏--------------------------------------------------------------------------------
# ┆ timestamp: 2022-09-26 14:28:06 -0600
# ┆ source: /spec/whoop_spec.rb:12
#
# Hello
# 
# ┗--------------------------------------------------------------------------------
whoop("My Label", color: :green) { "Hello" }

# log/debug.log (the colors don't appear in markdown)
# ┏------------------------------------ My Label ------------------------------------
# ┆ timestamp: 2022-09-26 14:28:06 -0600
# ┆ source: /spec/whoop_spec.rb:26
#
# Hello
# 
# ┗--------------------------------------------------------------------------------
whoop({hello: "world"}, format: :json, color: false)

# ┏--------------------------------------------------------------------------------
# ┆ timestamp: 2022-09-26 14:28:06 -0600
# ┆ source: /spec/whoop_spec.rb:39
#
# {
#   "hello": "world"
# }
# 
# ┗--------------------------------------------------------------------------------
user = User.first # or some object
whoop(user, format: :pretty)

# ┏--------------------------------------------------------------------------------
# ┆ timestamp: 2022-09-26 14:28:06 -0600
# ┆ source: /spec/whoop_spec.rb:39
#
# User {
#         :id => 1,
#       :name => "Eric",
#   :location => "Utah"
# }
# 
# ┗--------------------------------------------------------------------------------
whoop("This message includes context", color: false, context: {user: "Eric", ip_address: "127.0.0.1"})

# ┏--------------------------------------------------------------------------------
# ┆ timestamp: 2022-09-26 14:28:06 -0600
# ┆ source: /spec/whoop_spec.rb:39
# ┆ user: Eric
# ┆ ip_address: 127.0.0.1
#
# This message includes context
# 
# ┗--------------------------------------------------------------------------------
sql = 'SELECT emp_id, first_name,last_name,dept_id,mgr_id, ' +
      'WIDTH_BUCKET(department_id,20,40,10) "Exists in Dept" ' +
      'FROM emp WHERE mgr_id < 300 ORDER BY "Exists in Dept"'

whoop(sql, format: :sql)

# ┏--------------------------------------------------------------------------------
# ┆ timestamp: 2022-09-26 14:28:06 -0600
# ┆ source: /spec/whoop_spec.rb:52
#
# SELECT
#     emp_id
#     ,first_name
#     ,last_name
#     ,dept_id
#     ,mgr_id
#     ,WIDTH_BUCKET (
#       department_id
#       ,20
#       ,40
#       ,10
#     ) "Exists in Dept"
#   FROM
#     emp
#   WHERE
#     mgr_id < 300
#   ORDER BY
#     "Exists in Dept"
# 
# ┗--------------------------------------------------------------------------------

Auto-explain SQL queries

In addition to formatting the SQL query, you can also ask whoop to perform an explain on the query by using the explain: true argument.

Example (using the Example 1 sample plan from explain.dalibo.com):

sql = <<~SQL
    SELECT rel_users_exams.user_username AS rel_users_exams_user_username,
             rel_users_exams.exam_id AS rel_users_exams_exam_id,
             rel_users_exams.started_at AS rel_users_exams_started_at,
             rel_users_exams.finished_at AS rel_users_exams_finished_at,
             exam_1.id AS exam_1_id,
             exam_1.title AS exam_1_title,
             exam_1.date_from AS exam_1_date_from,
             exam_1.date_to AS exam_1_date_to,
             exam_1.created AS exam_1_created,
             exam_1.created_by_ AS exam_1_created_by_,
             exam_1.duration AS exam_1_duration,
             exam_1.success_threshold AS exam_1_success_threshold,
             exam_1.published AS exam_1_published
    FROM rel_users_exams LEFT OUTER
    JOIN exam AS exam_1
        ON exam_1.id = rel_users_exams.exam_id
    WHERE 1 = rel_users_exams.exam_id;
SQL

whoop("SQL with Explain", format: :sql, explain: true) { sql }

# ┏-------------------------------- SQL with Explain --------------------------------
# ┆ timestamp: 2022-09-26 14:50:11 -0600
# ┆ source: (irb):23:in `<top (required)>'
#
# sql:
#
# SELECT
#     rel_users_exams.user_username AS rel_users_exams_user_username
#     ,rel_users_exams.exam_id AS rel_users_exams_exam_id
#     ,rel_users_exams.started_at AS rel_users_exams_started_at
#     ,rel_users_exams.finished_at AS rel_users_exams_finished_at
#     ,exam_1.id AS exam_1_id
#     ,exam_1.title AS exam_1_title
#     ,exam_1.date_from AS exam_1_date_from
#     ,exam_1.date_to AS exam_1_date_to
#     ,exam_1.created AS exam_1_created
#     ,exam_1.created_by_ AS exam_1_created_by_
#     ,exam_1.duration AS exam_1_duration
#     ,exam_1.success_threshold AS exam_1_success_threshold
#     ,exam_1.published AS exam_1_published
#   FROM
#     rel_users_exams LEFT OUTER JOIN exam AS exam_1
#       ON exam_1.id = rel_users_exams.exam_id
#   WHERE
#     1 = rel_users_exams.exam_id
# ;
#
# query plan:
#
# Nested Loop Left Join  (cost=11.95..28.52 rows=5 width=157) (actual time=0.010..0.010 rows=0 loops=1)
#   Output: rel_users_exams.user_username, rel_users_exams.exam_id, rel_users_exams.started_at, rel_users_exams.finished_at, exam_1.id, exam_1.title, exam_1.date_from, exam_1.date_to, exam_1.created, exam_1.created_by_, exam_1.duration, exam_1.success_threshold, exam_1.published
#   Inner Unique: true
#   Join Filter: (exam_1.id = rel_users_exams.exam_id)
#   Buffers: shared hit=1
#   ->  Bitmap Heap Scan on public.rel_users_exams  (cost=11.80..20.27 rows=5 width=52) (actual time=0.009..0.009 rows=0 loops=1)
#         Output: rel_users_exams.user_username, rel_users_exams.exam_id, rel_users_exams.started_at, rel_users_exams.finished_at
#         Recheck Cond: (1 = rel_users_exams.exam_id)
#         Buffers: shared hit=1
#         ->  Bitmap Index Scan on rel_users_exams_pkey  (cost=0.00..11.80 rows=5 width=0) (actual time=0.005..0.005 rows=0 loops=1)
#               Index Cond: (1 = rel_users_exams.exam_id)
#               Buffers: shared hit=1
#   ->  Materialize  (cost=0.15..8.17 rows=1 width=105) (never executed)
#         Output: exam_1.id, exam_1.title, exam_1.date_from, exam_1.date_to, exam_1.created, exam_1.created_by_, exam_1.duration, exam_1.success_threshold, exam_1.published
#         ->  Index Scan using exam_pkey on public.exam exam_1  (cost=0.15..8.17 rows=1 width=105) (never executed)
#               Output: exam_1.id, exam_1.title, exam_1.date_from, exam_1.date_to, exam_1.created, exam_1.created_by_, exam_1.duration,
# exam_1.success_threshold, exam_1.published
#               Index Cond: (exam_1.id = 1)
# Planning Time: 1.110 ms
# Execution Time: 0.170 ms
# 
# ┗--------------------------------------------------------------------------------

Development

After checking out the repo, run bin/setup to install dependencies. Then, run rake spec to run the tests. You can also run bin/console for an interactive prompt that will allow you to experiment.

To install this gem onto your local machine, run bundle exec rake install. To release a new version, update the version number in version.rb, and then run bundle exec rake release, which will create a git tag for the version, push git commits and the created tag, and push the .gem file to rubygems.org.

Contributing

Bug reports and pull requests are welcome on GitHub at https://github.com/coderberry/whoop. This project is intended to be a safe, welcoming space for collaboration, and contributors are expected to adhere to the code of conduct.

License

The gem is available as open source under the terms of the MIT License.

Code of Conduct

Everyone interacting in the Whoop project's codebases, issue trackers, chat rooms and mailing lists is expected to follow the code of conduct.

Attribution

This project is maintained by Eric Berry.

This gem is based on the wrapped_print gem by Igor Kasyanchuk.