Sunday, October 24, 2010

Stack traces for Clojure app development

Edit (2011-Mar-06): This feature is available in Clj-MiscUtil as the bang operator.

The easiest way to print a stack trace in Clojure may be this:

user=> (Thread/dumpStack)
java.lang.Exception: Stack trace
at java.lang.Thread.dumpStack(Thread.java:1249)
at user$eval391.invoke(NO_SOURCE_FILE:193)
at clojure.lang.Compiler.eval(Compiler.java:5424)
at clojure.lang.Compiler.eval(Compiler.java:5391)
at clojure.core$eval.invoke(core.clj:2382)
at clojure.main$repl$read_eval_print__5624.invoke(main.clj:183)
at clojure.main$repl$fn__5629.invoke(main.clj:204)
at clojure.main$repl.doInvoke(main.clj:204)
at clojure.lang.RestFn.invoke(RestFn.java:422)
at clojure.main$repl_opt.invoke(main.clj:262)
at clojure.main$main.doInvoke(main.clj:355)
at clojure.lang.RestFn.invoke(RestFn.java:398)
at clojure.lang.Var.invoke(Var.java:361)
at clojure.lang.AFn.applyToHelper(AFn.java:159)
at clojure.lang.Var.applyTo(Var.java:482)
at clojure.main.main(main.java:37)
nil


However, many people realize that reading this kind of stack traces in Clojure is hard because they are intermingled with Java and Clojure implementation classes. It may help to filter the stack trace so that only relevant details appear. In this post we try to come up with an ad-hock filtering stack trace printer:

(defn get-stack-trace
([stack-trace]
(map #(let [class-name  (or (.getClassName  %) "")
method-name (or (.getMethodName %) "")
file-name   (or (.getFileName   %) "")
line-number (.getLineNumber %)]
[file-name line-number class-name method-name])
(into [] stack-trace)))
([]
(get-stack-trace (.getStackTrace (Thread/currentThread)))))


(defn get-clj-stack-trace
([classname-begin-tokens classname-not-begin-tokens]
(let [clj-stacktrace? (fn [[file-name line-number class-name method-name]]
(and (.contains file-name ".clj")
(or (empty? classname-begin-tokens)
(some #(.startsWith class-name %)
classname-begin-tokens))
(every? #(not (.startsWith class-name %))
classname-not-begin-tokens)))]
(filter clj-stacktrace? (get-stack-trace))))
([]
(get-clj-stack-trace [] ["clojure."])))


(defn print-table
[width-vector title-vector many-value-vectors]
(assert (= (type width-vector) (type title-vector) (type many-value-vectors)
(type [])))
(let [col-count (count width-vector)]
(assert (every? #(= (count %) col-count) many-value-vectors)))
(assert (= (count width-vector) (count title-vector)))
(let [fix-width (fn [text width]
(apply str
(take width (apply str text (take width (repeat " "))))))
sep-vector (into [] (map #(apply str (repeat % "-")) width-vector))]
(doseq [each (into [title-vector sep-vector] many-value-vectors)]
(doseq [i (take (count width-vector) (iterate inc 0))]
(print (fix-width (each i) (width-vector i)))
(print " | "))
(println))))


(defn print-stack-trace
([stack-trace-vector]
(print-table [20 5 45 10] ["File" "Line#" "Class" "Method"]
(into [] stack-trace-vector)))
([]
(print-stack-trace (get-clj-stack-trace))))


Having copy-pasted this code at the REPL, let us try to print the stack trace now:

user=> (print-stack-trace)
File                 | Line# | Class                                         | Method     |
-------------------- | ----- | --------------------------------------------- | ---------- |
nil


Well, that does not print anything because we have filtered out all non-Clojure stack trace; we have also filtered out all qualified class names beginning with "clojure." so that we can see the stack trace pertaining to application development only.

So let us tweak the command to print stack trace for all Clojure code at least:

user=> (print-stack-trace (get-clj-stack-trace [] []))
File                 | Line# | Class                                         | Method     |
-------------------- | ----- | --------------------------------------------- | ---------- |
core.clj             | 2382  | clojure.core$eval                             | invoke     |
main.clj             | 183   | clojure.main$repl$read_eval_print__5624       | invoke     |
main.clj             | 204   | clojure.main$repl$fn__5629                    | invoke     |
main.clj             | 204   | clojure.main$repl                             | doInvoke   |
main.clj             | 262   | clojure.main$repl_opt                         | invoke     |
main.clj             | 355   | clojure.main$main                             | doInvoke   |
nil


Now that stack trace is much easier to read! For a variation let us print the stack trace captured in an Exception:

user=> (print-stack-trace (get-stack-trace (.getStackTrace (Exception.))))
File                 | Line# | Class                                         | Method     |
-------------------- | ----- | --------------------------------------------- | ---------- |
NO_SOURCE_FILE       | 52    | user$eval52                                   | invoke     |
Compiler.java        | 5424  | clojure.lang.Compiler                         | eval       |
Compiler.java        | 5391  | clojure.lang.Compiler                         | eval       |
core.clj             | 2382  | clojure.core$eval                             | invoke     |
main.clj             | 183   | clojure.main$repl$read_eval_print__5624       | invoke     |
main.clj             | 204   | clojure.main$repl$fn__5629                    | invoke     |
main.clj             | 204   | clojure.main$repl                             | doInvoke   |
RestFn.java          | 422   | clojure.lang.RestFn                           | invoke     |
main.clj             | 262   | clojure.main$repl_opt                         | invoke     |
main.clj             | 355   | clojure.main$main                             | doInvoke   |
RestFn.java          | 398   | clojure.lang.RestFn                           | invoke     |
Var.java             | 361   | clojure.lang.Var                              | invoke     |
AFn.java             | 159   | clojure.lang.AFn                              | applyToHel |
Var.java             | 482   | clojure.lang.Var                              | applyTo    |
main.java            | 37    | clojure.main                                  | main       |
nil


You can try embedding the functions listed here in an application project and then print the stack trace using (print-stack-trace) - it will display only those lines available/relevant in your project.

Feedback/comments are welcome. You may like to follow me on Twitter.

No comments:

Post a Comment