io.aviso/pretty¶
Sometimes, neatness counts¶
If you are trying to puzzle out a stack trace, pick a critical line of text out of a long stream of console output, or compare two streams of binary data, a little bit of formatting can go a long way.
That’s what the io.aviso/pretty library is for. It adds support for pretty output where it counts:
- Readable output for exceptions
- ANSI font and background color support
- Hex dump of binary data
- Hex dump of binary deltas
- Formatting data into columns
Here’s an example of pretty at work:
License¶
Pretty is released under the terms of the Apache Software License 2.0.
ANSI Formatting¶
The io.aviso.ansi
namespace defines a number of functions and constants for producing
ANSI escape codes.
For each of the supported colors (black, red, green, yellow, blue, magenta, cyan, and white) there will be four functions and four constants:
- color - function to set text color
- color-bg - function to set background color
- bold-color - function to set enable bold text and the text color
- bold-color-bg - function to enable bold text and the background color
- color-font - constant that enables the text color
- color-bg-font - constant that enables the color as background
- bold-color-font - constant that enables the text color in bold
- bold-color-bg-font - constant that enables the bold color as background
The functions are passed a string and wrap the string with ANSI codes to enable an ANSI graphic representation for the text, with a reset after the text.
Note that the exact color interpretation of the ANSI codes varies significantly between platforms and applications, and is frequently configurable, often using themes. You may need to adjust your application’s settings to get an optimum display.
In addition there are functions bold, inverse, and italic and constants bold-font, inverse-font, italic-font, and reset-font.
The above example could also be written as:
(println (str "The following text will be " bold-red-font "bold and red" reset-font "."))
Formatted Exceptions¶
Pretty’s main focus is on formatting of exceptions for readability, addressing one of Clojure’s core weaknesses.
Rationale¶
Exceptions in Clojure are extremely painful for many reasons:
- They are often nested (wrapped and rethrown)
- Stack traces are output for every exception, which clogs output without providing useful detail
- Stack traces are often truncated, requiring the user to manually re-assemble the stack trace from several pieces
- Many stack frames represent implementation details of Clojure that are not relevant
This is addressed by the io.aviso.exception/write-exception
function; it take an exception
and writes it to the console, *out*
.
This is best explained by example; here’s a SQLException wrapped inside two RuntimeExceptions, and printed normally:
user=> (throw (make-ex-info))
Execution error (SQLException) at user/jdbc-update (REPL:1).
Database failure
SELECT FOO, BAR, BAZ
FROM GNIP
failed with ABC123
user=> (pst)
SQLException Database failure
SELECT FOO, BAR, BAZ
FROM GNIP
failed with ABC123
user/jdbc-update (NO_SOURCE_FILE:1)
user/jdbc-update (NO_SOURCE_FILE:1)
user/make-jdbc-update-worker/reify--169 (NO_SOURCE_FILE:5)
user/update-row (NO_SOURCE_FILE:4)
user/update-row (NO_SOURCE_FILE:1)
user/make-exception (NO_SOURCE_FILE:5)
user/make-exception (NO_SOURCE_FILE:1)
user/make-ex-info (NO_SOURCE_FILE:5)
user/make-ex-info (NO_SOURCE_FILE:1)
user/eval175 (NO_SOURCE_FILE:1)
user/eval175 (NO_SOURCE_FILE:1)
clojure.lang.Compiler.eval (Compiler.java:7176)
nil
user=> *clojure-version*
{:major 1, :minor 10, :incremental 0, :qualifier nil}
user=>
This is greatly improved in Clojure 1.10 over prior Clojure releases, but still quite minimal.
On a good day, the exception messages will include all the details you need to resolve the problem … even though
Clojure encourages you to use the ex-info
to create an exception,
which puts important data into properties of the exception, which are not normally printed.
Meanwhile, you will have to mentally scan and parse the above text explosion, to parse out file names and line numbers.
It’s one more bit of cognitive load you just don’t need in your day.
Instead, here’s the equivalent, using a hooked version of Clojure’s clojure.repl/pst
,
modified to use write-exception
.
As you can see, this lets you focus in on the exact cause and location of your problem.
write-exception
flips around the traditional order, providing a chronologically sequential view:
- The stack trace leading to the root exception comes first, and is ordered outermost frame to innermost frame.
- The exception stack comes after the stack trace, and is ordered root exception (innermost) to outermost, reflecting how the stack has unwound, and the root exception was wrapped in new exceptions and rethrown.
The stack trace is carefully formatted for readability, with the left-most column identifying Clojure functions or Java class and method, and the right columns presenting the file name and line number.
The stack frames themselves are filtered to remove details that are not relevant. This filtering is via an optional function, so you can define filters that make sense for your code. For example, the default filter omits frames in the clojure.lang package (they are reduced to ellipses), and truncates the stack trace when when it reaches clojure.main/repl/read-eval-print.
Repeating stack frames are also identified and reduced to a single line (that identifies the number of frames). This allows your infinite loop that terminates with a StackOverflowException to be reported in just a few lines, not thousands.
The inverted (from Java norms) ordering has several benefits:
- Chronological order is maintained, whereas a Java stack trace is in reverse chronological order.
- The most relevant details are at (or near) the bottom not the top; this means less “scrolling back to see what happened”.
The related function, format-exception
, produces the same output, but returns it as a string.
For both format-exception
and write-exception
, output of the stack trace is optional, or can be limited to a certain number of stack frames.
Frames can also be highlighted by customizing io.aviso.exception/*app-frame-names*
. This adds extra visual clarity to identify frames that belong in your Clojure
code vs. library code.
Before:
After:
Notice with custom app-frame-names, the matched frame names are also in bold font. This is customized by re-binding or altering
*app-frame-names*
, which is a list of string or patterns to match on the frame’s name.
;; marks any frame that begins with demo
(alter-var-root #'io.aviso.exception/*app-frame-names* (constantly [#"my-app.*"]))
io.aviso.repl¶
This namespace includes a function, install-pretty-exceptions
, which
hooks into all the common ways that exceptions are output in Clojure and redirects them to use write-exception.
When exceptions occur, they are printed out without a stack trace or properties.
The clojure.repl/pst
function is overridden to fully print the exception (with properties and stack trace).
In addition, clojure.stacktrace/print-stack-trace
and clojure.stacktrace/print-cause-trace
are overwritten; these
are used by clojure.test
. Both do the same thing: print out the full exception (again,
with properties and stack trace).
You may not need to invoke this directly, as pretty can also act as a Leiningen Plugin.
io.aviso.logging¶
This namespace includes functions to change clojure.tools.logging
to use Pretty to output exceptions, and to add a
default Thread.UncaughtExceptionHandler that uses clojure.tools.logging
.
REPL¶
Pretty includes some useful REPL utilities in the io.aviso.repl
namespace.
Primarily, the install-pretty-exceptions
function overrides several
internal Clojure functions to enable
formatted exceptions.
This function is normally invoked for you
when Pretty is
used as a Leiningen plugin.
The remaining functions exist to help you make things pretty that don’t originate in the REPL. You will often see output in logs: EDN data perhaps, or often, raw exceptions.
If you have a REPL running, you can use the following functions to get a better view of that data:
copy¶
The copy
function will return the current contents of the system clipboard
as a string.
This requires that AWT is running.
On OS X, you will see a window for your application start when you first invoke this function
The pretty-print
and format-exception
functions can be invoked
with no arguments, in which case the call to copy
happens automatically.
Consult the API documentation for more details.
pretty-print¶
This will pretty-print the contents of the clipboard; the clipboard text is parsed as EDN.
format-exception¶
This will parse a normal Java stack trace and format it for readability.
paste¶
This will copy a string back on to the clipboard, so it can be pasted into another window.
(use 'io.aviso.repl)
(-> (copy) format-exception paste)
Leiningen Plugin¶
pretty can act as a plugin to Leiningen.
To enable pretty exception reporting automatically, add
pretty to both the :plugins and the :dependencies lists
of your project.clj
.
(defproject ...
:plugins [[io.aviso/pretty "X.Y.Z"]]
:middleware [io.aviso.lein-pretty/inject]
:dependencies [...
[io.aviso/pretty "X.Y.Z"]]
...)
Adjust the version number for the current version, “0.1.37”.
Tip
Often, you only add io.aviso/pretty
to your :dev profile dependencies.
This adds middleware to enable pretty exception reporting when running a REPL, tests, or anything else that starts code in the project.
Another option is to add the following to your ~/.lein/profiles.clj
:
:pretty {
:plugins [[io.aviso/pretty "X.Y.Z"]]
:dependencies [[io.aviso/pretty "X.Y.Z"]]
:middleware [io.aviso.lein-pretty/inject]
}
This creates an opt-in profile that adds and enables pretty exception reporting.
You can then enable pretty in any project, even one that does not normally have pretty as a dependency, as follows:
lein with-profiles +pretty run
or:
lein with-profiles +pretty do clean, test, install
You may also want to add the following to your ~/.bash_profile
:
alias pretty="lein with-profile +pretty"
At which point, you can use the command pretty
instead of lein
.
Binary Output¶
The io.aviso.binary
namespace provides support output of binary data.
Binary data is represented using the protocol BinaryData; this protocol is extended on byte arrays, on String, and on nil. BinaryData is simply a randomly accessible collection of bytes, with a known length.
(write-binary "Choose immutability and see where it takes you.")
0000: 43 68 6F 6F 73 65 20 69 6D 6D 75 74 61 62 69 6C 69 74 79 20 61 6E 64 20 73 65 65 20 77 68 65 72
0020: 65 20 69 74 20 74 61 6B 65 73 20 79 6F 75 2E
Alternately, format-binary
will return the formatted binary output string.
You can also compare two binary data values with write-binary-delta
:
If the two data are of different lengths, the shorter one is padded with --
to make up the difference.
As with write-binary
, there’s a format-binary-delta
.
Columnar Output¶
The io.aviso.columns
namespace is what’s used by the exceptions namespace to format the exceptions, properties, and stack traces.
The format-columns
function is provided with a number of column definitions, each of which describes the width and justification of a column.
Some column definitions are just a string to be written for that column, such as a column separator.
format-columns
returns a function that accepts the column values and writes the formatted column data to out.
write-rows
takes the function provided by format-columns
, plus a set of functions to extract column values,
plus a seq of rows.
In most cases, the rows are maps, and the extraction functions are keywords (isn’t Clojure magical that way?).
Here’s an example, based on code in the exception namespace:
(defn ^:private write-stack-trace
[exception]
(let [elements (->> exception expand-stack-trace (map preformat-stack-frame))
formatter (c/format-columns [:right (c/max-value-length elements :formatted-name)]
" " (:source *fonts*)
[:right (c/max-value-length elements :file)]
2
[:right (->> elements (map :line) (map str) c/max-length)]
(:reset *fonts*))]
(c/write-rows formatter
[:formatted-name
:file
#(if (:line %) ": ")
:line]
elements)))