• Stars
    star
    156
  • Rank 239,589 (Top 5 %)
  • Language
    Clojure
  • License
    Eclipse Public Li...
  • Created about 6 years ago
  • Updated about 2 years ago

Reviews

There are no reviews yet. Be the first to send feedback to the community and the maintainers!

Repository Details

A simple debug library for Clojure(Script) that features data-oriented logging and tracing

Postmortem

Clojars Project build codecov bb compatible

A simple debug library for Clojure(Script) that features data-oriented logging and tracing

Features

  • Postmortem strongly encourages the data-oriented debugging approach
    • Logs are just Clojure data, so you can use DataScript, REBL or whatever tools for more sophisticated log analysis
  • Integration with transducers enables various flexible logging strategies
  • Instrumentation on vars makes it easier to debug functions without touching their code
  • Supports most of Clojure platforms (namely, Clojure, ClojureScript, self-hosted ClojureScript and Babashka)
  • Possible to use for debugging multi-threaded programs

Synopsis

(require '[postmortem.core :as pm]
         '[postmortem.xforms :as xf])

(defn sum [n]
  (loop [i n sum 0]
    (pm/dump :sum (xf/take-last 5))
    (if (= i 0)
      sum
      (recur (dec i) (+ i sum)))))

(sum 100) ;=> 5050

(pm/log-for :sum)
;=> [{:n 100, :i 4, :sum 5040}
;    {:n 100, :i 3, :sum 5044}
;    {:n 100, :i 2, :sum 5047}
;    {:n 100, :i 1, :sum 5049}
;    {:n 100, :i 0, :sum 5050}]


(require '[postmortem.instrument :as pi])

(defn broken-factorial [n]
  (cond (= n 0) 1
        (= n 7) (/ (broken-factorial (dec n)) 0) ;; <- BUG HERE!!
        :else (* n (broken-factorial (dec n)))))

(pi/instrument `broken-factorial
               {:xform (comp (xf/take-until :err) (xf/take-last 5))})

(broken-factorial 10)
;; Execution error (ArithmeticException) at user/broken-factorial.
;; Divide by zero

(pm/log-for `broken-factorial)
;=> [{:args (3), :ret 6}
;    {:args (4), :ret 24}
;    {:args (5), :ret 120}
;    {:args (6), :ret 720}
;    {:args (7), :err #error {:cause "Divide by zero" ...}}]

Table of Contents

Requirements

  • Clojure 1.8+, or
  • ClojureScript 1.10.238+, or
  • Babashka v0.10.163+, or
  • Planck 2.24.0+, or
  • Lumo 1.10.1+

We have only tested on the above environments, but you could possibly use the library on some older versions of the ClojureScript runtimes as well.

Installation

Add the following to your project dev dependencies or the :user profile in ~/.lein/profiles.clj:

Clojars Project

Usage

Basic usage

spy>> / log-for

In Postmortem, spy>> and log-for are two of the most fundamental functions. spy>> is for logging data, and log-for is for retrieving logged data.

spy>> is used as follows:

(require '[postmortem.core :as pm])

(defn sum [n]
  (loop [i 0 sum 0]
    (if (> i n)
      sum
      (recur (inc i)
             (pm/spy>> :sum (+ i sum))))))

(spy>> <key> <expr>) stores the value of the <expr> to a log entry for the key <key> each time the spy>> form is evaluated. In the above example, (pm/spy>> :sum (+ i sum)) will store intermediate results of summation for each iteration to the log entry for the key :sum.

(log-for <key>), on the other hand, retrieves all the logged data stored in the log entry for the key <key>. In the following example, (log-for :sum) results in [0 1 3 6 10 15], which corresponds to the intermediate summations from 0 to 5:

(sum 5)
;=> 15

(pm/log-for :sum)
;=> [0 1 3 6 10 15]

Any Clojure data can be used as a log entry key, such as keywords (as in the above examples), symbols, integers, strings or whatever. In fact, you can even use a runtime value as a key, as well as literal values, and thus entry keys can also be used as a handy way to collect and group log data:

(defn f [n]
  (pm/spy>> [:f (even? n)] (inc n)))

(mapv f [1 2 3 4 5])
;=> [2 3 4 5 6]
(pm/log-for [:f true])
;=> [3 5]
(pm/log-for [:f false])
;=> [2 4 6]

reset-key! / completed? / keys

To clear the logged data at the log entry <key>, call (reset-key! <key>):

(pm/log-for :sum)
;=> [0 1 3 6 10 15]

(pm/reset-key! :sum)

(pm/log-for :sum)
;=> nil

Note that once you call log-for for a key k, the log entry for k will be completed. A completed log entry will not be changed anymore until you call reset-key! for the log entry k:

(pm/spy>> :foobar 1)
(pm/spy>> :foobar 2)
(pm/log-for :foobar)
;=> [1 2]

(pm/spy>> :foobar 3)
(pm/spy>> :foobar 4)
(pm/log-for :foobar)
;=> [1 2]

(pm/reset-key! :foobar)

(pm/spy>> :foobar 3)
(pm/spy>> :foobar 4)
(pm/log-for :foobar)
;=> [3 4]

You can check if a log entry has been completed using (completed? <key>):

(pm/spy>> :barbaz 10)
(pm/spy>> :barbaz 20)
(pm/completed? :barbaz)
;=> false

(pm/log-for :barbaz)
;=> [10 20]
(pm/completed? :barbaz)
;=> true

(pm/reset-key! :barbaz)
(pm/completed? :barbaz)
;=> false

If you want to know what log entry keys have been logged so far without completing any log entry, keys suits your desire:

(pm/spy>> :bazqux 10)
(pm/spy>> :quxquux 20)

(pm/keys)
;=> #{:bazqux :quxquux}
(pm/completed? :bazqux)
;=> false
(pm/completed? :quxquux)
;=> false

logs / stats / reset!

You can also logs some data to more than one log entries at once. In such a case, logs is more useful to look into the whole log data than just calling log-for for each log entry:

(defn sum [n]
  (loop [i 0 sum 0]
    (pm/spy>> :i i)
    (if (> i n)
      sum
      (recur (inc i)
             (pm/spy>> :sum (+ i sum))))))

(sum 5)
;=> 15

(pm/logs)
;=> {:i [0 1 2 3 4 5 6],
;    :sum [0 1 3 6 10 15]}

Alternatively, stats helps you grasp how many log items have been stored so far for each log entry key, without seeing the actual log data:

(defn sum [n]
  (loop [i 0 sum 0]
    (pm/spy>> :i i)
    (if (> i n)
      sum
      (recur (inc i)
             (pm/spy>> :sum (+ i sum))))))

(sum 5) ;=> 15

(pm/stats)
;=> {:i 7 :sum 6}

;; As compared to:
;; (pm/logs)
;; => {:i [0 1 2 3 4 5 6],
;      :sum [0 1 3 6 10 15]}

Note that once you call stats, all the log entries will be completed, as with the logs fn.

For those who are using older versions (<= 0.4.0), pm/stats is the new name for pm/frequencies added in 0.4.1. They can be used totally interchangeably. Now pm/stats is recommended for primary use.

Analogous to logs, reset! is useful to clear the whole log data at a time, rather than clearing each individual log entry one by one calling reset-key!:

(pm/logs)
;=> {:i [0 1 2 3 4 5 6],
;    :sum [0 1 3 6 10 15]}

(pm/reset!)

(pm/logs)
;=> {}

spy>

spy>> has a look-alike cousin named spy>. They have no semantic difference, except that spy> is primarily intended to be used in thread-first contexts and therefore takes the log data as its first argument while spy>> is mainly intended to be used in thread-last contexts and therefore takes the log data as its last argument.

For example, the following two expressions behave in exactly the same way:

;; thread-last version
(->> (+ 1 2)
     (pm/spy>> :sum)
     (* 10)
     (pm/spy>> :prod))

;; thread-first version
(-> (+ 1 2)
    (pm/spy> :sum)
    (* 10)
    (pm/spy> :prod))

dump

dump is another convenient tool to take snapshots of the values of local bindings.

(dump <key>) stores a local environment map to the log entry <key>. A local environment map is a map of keywords representing local names in the scope at the callsite, to the values that the corresponding local names are bound to.

The example code below shows how dump logs the values of the local bindings at the callsite (namely, n, i and sum) for each iteration:

(defn sum [n]
  (loop [i 0 sum 0]
    (pm/dump :sum)
    (if (> i n)
      sum
      (recur (inc i) (+ i sum)))))

(sum 5)
;=> 15

(pm/log-for :sum)
;=> [{:n 5, :i 0, :sum 0}
;    {:n 5, :i 1, :sum 0}
;    {:n 5, :i 2, :sum 1}
;    {:n 5, :i 3, :sum 3}
;    {:n 5, :i 4, :sum 6}
;    {:n 5, :i 5, :sum 10}
;    {:n 5, :i 6, :sum 15}]

Integration with transducers

After reading this document so far, you may wonder what if the loop would be repeated millions of times? What if you only need the last few log items out of them?

That's where Postmortem really shines. It achieves extremely flexible customizability of logging strategies by integration with transducers (If you are not familiar with transducers, we recommend that you take a look at the official reference first).

Postmortem's logging operators (spy>>, spy> and dump) are optionally takes a transducer after the log entry key. When you call (spy>> <key> <xform> <expr>), the transducer <xform> controls whether or not the given data will be logged and/or what data will actually be stored.

For example:

(defn sum1 [n]
  (loop [i 0 sum 0]
    (if (> i n)
      sum
      (recur (inc i)
             (pm/spy>> :sum1 (+ i sum))))))

(defn sum2 [n]
  (loop [i 0 sum 0]
    (if (> i n)
      sum
      (recur (inc i)
             (pm/spy>> :sum2 (filter odd?) (+ i sum))))))

(sum1 5) ;=> 15
(sum2 5) ;=> 15

(pm/log-for :sum1)
;=> [0 1 3 6 10 15]

(pm/log-for :sum2)
;=> [1 3 15]

You see two invocations to spy>> in the example code. The first one is an ordinary invocation without a transducer. The second one is called with a transducer (filter odd?). With that transducer, the log entry for the key :sum2 only stores odd numbers while the entry for :sum1 holds every intermediate sum result.

Not only filter, you can use any transducer to customize how a log item will be stored. The following code uses a transducer (map (fn [m] (select-keys m [:sum]))), which only stores the value of the local binding sum for each iteration:

(defn sum [n]
  (loop [i 0 sum 0]
    (pm/dump :sum (map (fn [m] (select-keys m [:sum]))))
    (if (> i n)
      sum
      (recur (inc i) (+ i sum)))))

(sum 5) ;=> 15

(pm/log-for :sum)
;=> [{:sum 0}
;    {:sum 0}
;    {:sum 1}
;    {:sum 3}
;    {:sum 6}
;    {:sum 10}
;    {:sum 15}]

Also, transducers can be used to restrict the maximum log size. For example, (take <n>) only allows the first up to <n> items to be logged:

(defn sum [n]
  (loop [i 0 sum 0]
    (pm/dump :sum (take 3))
    (if (> i n)
      sum
      (recur (inc i) (+ i sum)))))

(sum 5) ;=> 15

(pm/log-for :sum)
;=> [{:n 5, :i 0, :sum 0} {:n 5, :i 1, :sum 0} {:n 5, :i 2, :sum 1}]

(drop-while <pred>) would drop log data until <pred> returns false:

(defn sum [n]
  (loop [i 0 sum 0]
    (pm/dump :sum (drop-while (fn [{:keys [sum]}] (< sum 5))))
    (if (> i n)
      sum
      (recur (inc i) (+ i sum)))))

(sum 5) ;=> 15

(pm/log-for :Sum)
;=> [{:n 5, :i 4, :sum 6} {:n 5, :i 5, :sum 10} {:n 5, :i 6, :sum 15}]

You can even pick up logs by random sampling using (random-sample <prob>):

(defn sum [n]
  (loop [i 0 sum 0]
    (pm/dump :sum (random-sample 0.3))
    (if (> i n)
      sum
      (recur (inc i) (+ i sum)))))

(sum 5) ;=> 15

(pm/log-for :sum)
;=> [{:n 5, :i 0, :sum 0} {:n 5, :i 3, :sum 3}]

(pm/reset!)
(sum 5) ;=> 15

(pm/log-for :sum)
;=> [{:n 5, :i 2, :sum 1} {:n 5, :i 4, :sum 6} {:n 5, :i 5, :sum 10}]

Postmortem also has its own set of utility transducers. The namespace postmortem.xforms provides several useful transducers to implement specific logging strategies.

take-last is one of the most useful transducer of those. (take-last <n>) just logs the last <n> items, which only requires a fixed-size buffer (rather than an indefinite-size one) to store the desired range of logs even when the logging operator is repeatedly invoked millions of times:

(require '[postmortem.xforms :as xf])

(defn sum [n]
  (loop [i 0 sum 0]
    (pm/dump :sum (xf/take-last 5))
    (if (> i n)
      sum
      (recur (inc i) (+ i sum)))))

(sum 1000000) ;=> 500000500000

(pm/log-for :sum)
;=> [{:n 1000000, :i 999997, :sum 499996500006}
;    {:n 1000000, :i 999998, :sum 499997500003}
;    {:n 1000000, :i 999999, :sum 499998500001}
;    {:n 1000000, :i 1000000, :sum 499999500000}
;    {:n 1000000, :i 1000001, :sum 500000500000}]

Sessions

A session is an abstraction responsible for what actually happens when storing and retrieving logs and where the actual log data will be stored. It can be used to completely isolate some logs from the other, or to enable/disable the entire logging mechanism, etc.

Handling sessions

Postmortem's logging operators takes another optional argument for session. For example, (spy>> <session> <key> <expr> <xforms>) stores logs into the <session>.

To make a new session, use make-session:

(def sess (pm/make-session))

(pm/spy>> sess :foo 1 identity)
(pm/spy>> sess :bar 2 identity)
(pm/spy>> sess :foo 3 identity)

Note that to specify your session explicitly, you'll need to specify a transducer even if you don't really want to change a logging strategy using it. Here, the identity transducer is specified as a placeholder.

To retrieve log data from the session sess, call log-for / logs with it:

(pm/log-for sess :foo)
;=> [1 3]
(pm/logs sess)
;=> {:foo [1 3) :bar [2]]}

When you omit a session, things behave as if the current session were specified. The current session is the default session that can be accessed globally.

To get the current session, use current-session:

(pm/current-session)
;; Returns the current session object

And the following pairs of expressions are semantically identical, respectively:

(pm/spy>> :foo identity {:foo 42})
(pm/spy>> (pm/current-session) :foo identity {:foo 42})

(pm/dump :foo identity)
(pm/dump (pm/current-session) :foo identity)

(pm/log-for :foo)
(pm/log-for (pm/current-session) :foo)

(pm/logs)
(pm/logs (pm/current-session))

To set the current session to your own session, you can use set-current-session!:

(def sess (pm/make-session))
(pm/set-current-session! sess)

(pm/spy>> :foo 1)
(pm/spy>> :foo 2)
(pm/spy>> :foo 3)

(pm/log-for :foo)
;=> [1 2 3]
(pm/log-for sess :foo)
;=> [1 2 3]

Or you can temporarily change the current session with with-session:

(def sess (pm/make-session))

(pm/spy>> :foo 1)
(pm/with-session sess
  (pm/spy>> :foo 2)
  (pm/spy>> :foo 3))
(pm/spy>> :foo 4)

(pm/log-for :foo)
;=> [1 4]
(pm/log-for sess :foo)
;=> [2 3]

Attaching a transducer

Transducers can be attached to sessions as well. Those transducers attached to a session are called a base transducer of the session. To make a session with a base transducer attached, call (make-session <xform>). If a session has a base transducer, a logging operator operating on the session will behave as if it were called (1) with that base transducer, or (2) with a transducer produced by prepending (a la comp) the base transducer to the transducer that is originally passed to the logging operator, if any.

For example for case 1, this code

(pm/set-current-session! (pm/make-session (take 5)))

(pm/dump :key)

is equivalent to the following:

(pm/set-current-session! (pm/make-session))

(pm/dump :key (take 5))

And for case 2, this

(pm/set-current-session! (pm/make-session (drop 5)))

(pm/dump :key (take 5))

is equivalent to:

(pm/set-current-session! (pm/make-session))

(pm/dump :key (comp (drop 5) (take 5)))

This feature is useful to apply a common transducer to all the logging operators operating on the same session.

void-session

A void session is another implementation of Postmortem session which does nothing at all. It's useful to disable the logging operators operating on the current session.

To get the void session, call void-session:

(pm/set-current-session! (pm/void-session))

(pm/spy>> :foo 1)
(pm/spy>> :foo 2)
(pm/spy>> :foo 3)

(pm/log-for :foo)
;=> []

Using it together with with-session disables logging temporarily:

(pm/set-current-session! (pm/make-session))

(pm/spy>> :foo 1)
(pm/with-session (pm/void-session)
  (pm/spy>> :foo 2)
  (pm/spy>> :foo 3))
(pm/spy>> :foo 4)

(pm/log-for :foo)
;=> [1 4]

Indexed sessions

When dealing with multiple log entries, it is sometimes useful to have a sequential number (or index) for each log item throughout all the entries.

An indexed session automatically adds an auto-increment index to each log item. To create a new indexed session, use make-indexed-session:

(pm/set-current-session! (pm/make-indexed-session))

(pm/spy>> :foo 100)
(pm/spy>> :bar 101)
(pm/spy>> :foo 102)

(pm/logs)
;=> {:foo [{:id 0 :val 100}
;          {:id 2 :val 102}]
;    :bar [{:id 1 :val 101}]}

Calling reset! on the indexed session resets the index:

(pm/spy>> :foo 100)
(pm/spy>> :foo 101)
(pm/log-for :foo)
;=> [{:id 0 :val 100} {:id 1 :val 101}]

(pm/reset!)

(pm/spy>> :foo 102)
(pm/spy>> :foo 103)
(pm/log-for :foo)
;=> [{:id 0 :val 102} {:id 1 :val 103}]

make-indexed-session takes an optional function to specify how the indexed session will attach the index to each log item.

The function must take two arguments, the index and the log item, and return a new log item. The default function is (fn [id item] {:id id :val item}).

The example below shows how it takes effect:

(pm/set-current-session!
  (pm/make-indexed-session (fn [id item] [id item])))

(pm/spy>> :foo :a)
(pm/spy>> :foo :b)
(pm/spy>> :foo :c)
(pm/log-for :foo)
;=> [[0 :a] [1 :b] [2 :c]]

(pm/set-current-session!
  (pm/make-indexed-session #(assoc %2 :i %1)))

(pm/spy>> :point {:x 100 :y 100})
(pm/spy>> :point {:x 200 :y 200})
(pm/spy>> :point {:x 300 :y 300})
(pm/log-for :point)
;=> [{:i 0 :x 100 :y 100}
;    {:i 1 :x 200 :y 200}
;    {:i 2 :x 300 :y 300}]

The indexed function is another way to create an indexed session. (indexed <session>) creates a new indexed session based on another session. In fact, (make-indexed-session) is equivalent to (indexed (make-session)).

It's especially useful to make an session with base transducer into an indexed session:

(pm/set-current-session!
  (pm/indexed (pm/make-session (take-while #(< (:id %) 3)))))

(doseq [v [:a :b :c :d :e]]
  (pm/spy>> :foo v))
(pm/log-for :foo)
;=> [{:id 0 :val :a}
;    {:id 1 :val :b}
;    {:id 2 :val :c}]

make-unsafe-session

In Clojure, an ordinary session (created by make-session) is inherently thread safe, so you can safely share and update it across more than one threads:

(def sess (pm/make-session))
(pm/set-current-session! sess)

(defn f [n]
  (pm/dump :f))

(run! deref [(future (dotimes [i 10000] (f i)))
             (future (dotimes [i 10000] (f i)))])

(count (pm/log-for sess :f)) ;=> 20000

This thread safety is achieved by means of pessimistic locking during the session update. If it is guaranteed that no more than one updates never happen simultaneously on a single session, you can use make-unsafe-session instead to avoid the overhead of mutual exclusion. make-unsafe-session behaves almost same as make-session except for thread safety and performance.

;; you can use `make-unsafe-session` in the same way as `make-session`
(def sess (pm/make-unsafe-session))
(pm/set-current-session! sess)

(pm/spy>> :foo 1)
(pm/spy>> :foo 2)
(pm/spy>> :foo 3)

(pm/log-for sess :foo) ;=> [1 2 3]

;; but `make-unsafe-session` is not thread safe
(defn f [n]
  (pm/dump :f))

(run! deref [(future (dotimes [i 10000] (f i)))
             (future (dotimes [i 10000] (f i)))])

(count (pm/log-for sess :f)) ;=> 11055

In ClojureScript, make-session is completely identical to make-unsafe-session.

Simple logger

Postmortem 0.5.0+ provides a new feature, simple loggers. A simple logger essentially works like spy>> and log-for, but offers a more simplified API for common use cases.

A simple logger is implemented as a function with two arities that closes over an implicit session:

(def f (pm/make-logger))

(f 1)
(f 2)
(f 3)
(f 4)
(f) ;=> [1 2 3 4]

As you may see, if a simple logger is called with one argument, it acts like (spy>> :key <argument>) on the implicit session whereas if called with no argument, it acts like (log-for :key).

If you create a simple logger by passing an transducer as the optional argument, it will behave as if you attached that transducer to the implicit session:

(def f (pm/make-logger (map #(* % %))))

(f 1)
(f 2)
(f 3)
(f 4)
(f) ;=> [1 4 9 16]

A multi logger is a variant of the simple logger that has three arities, i.e. 2-arg for (spy>> <arg1> <arg2>), 1-arg for (log-for <arg>) and 0-arg for (logs).

(def f (pm/make-multi-logger))

(loop [n 5, sum 0]
  (if (= n 0)
    sum
    (recur (f :n (dec n)) (f :sum (+ sum n)))))
;=> 15

(f) ;=> {:n [4 3 2 1 0], :sum [5 9 12 14 15]}
(f :n) ;=> [4 3 2 1 0]
(f :sum) ;=> [5 9 12 14 15]

Instrumentation

Postmortem has one more powerful feature: instrumentation. It looks like clojure.spec's feature with the same name. Once you instrument a function, you can collect execution log for it without touching its code.

To use the instrumentation feature, you'll need to require the postmortem.instrument namespace. The namespace provides two macros, instrument and unstrument, which enables/disables logging for a specified var, respectively.

(require '[postmortem.core :as pm]
         '[postmortem.instrument :as pi])

(defn f [x] (inc x))

;; Instruments `f` to enable logging
(pi/instrument `f)
;=> [user/f]

(dotimes [i 5] (prn (f i)))

(pm/log-for `f)
;=> [{:args (0)}
;    {:args (0), :ret 1}
;    {:args (1)}
;    {:args (1), :ret 2}
;    {:args (2)}
;    {:args (2), :ret 3}
;    {:args (3)}
;    {:args (3), :ret 4}
;    {:args (4)}
;    {:args (4), :ret 5}]

;; Unstruments `f` to disable logging
(pi/unstrument `f)
;=> [user/f]

(pm/reset!)

(dotimes [i 5] (prn (f i)))
(pm/log-for `f) ;=> nil

As you can see, the execution log for an instrumented function consists of two types of log items, ones for entry and ones for exit. An entry log item is logged immediately after the function gets called whereas an exit log item is logged immediately after the function either returns or throws.

Both types of the log items contain the :args key that represents the arguments passed to the function when it's called. An exit log item contains an extra key: If the function exits normally, the corresponding exit log item will have the :ret key that holds the return value, and otherwise (i.e. if the function fails) the exit log item will have the :err key that holds the error object thrown in the function.

Virtually, instrumenting a function f replaces f with something like this:

(fn [& args]
  (pm/spy>> `f {:args args})
  (try
    (let [ret (apply f args)]
      (pm/spy>> `f {:args args :ret ret}))
    (catch Throwable t
      (pm/spy>> `f {:args args :err t}))))

This error handling behavior may be useful to identify which function call actually caused an error especially when you are debugging a recursive function:

(defn broken-factorial [n]
  (cond (= n 0) 1
        (= n 5) (/ (broken-factorial (dec n)) 0) ;; <- BUG HERE!!
        :else (* n (broken-factorial (dec n)))))

;; So far so good
(map broken-factorial (range 5))
;=> (1 1 2 6 24)

;; Boom!!
(broken-factorial 7)
;; Execution error (ArithmeticException) at user/broken-factorial.
;; Divide by zero

;; Now let's look into it further!
(pi/instrument `broken-factorial)
;=> [user/broken-factorial]

(broken-factorial 7) ;; throws error
(pm/log-for `broken-factorial)
;=> [{:args (7)}
;    {:args (6)}
;    {:args (5)}
;    {:args (4)}
;    {:args (3)}
;    {:args (2)}
;    {:args (1)}
;    {:args (0)}
;    {:args (0), :ret 1}
;    {:args (1), :ret 1}
;    {:args (2), :ret 2}
;    {:args (3), :ret 6}
;    {:args (4), :ret 24}
;    {:args (5), :err #error {:cause "Divide by zero" ...}}
;    {:args (6), :err #error {:cause "Divide by zero" ...}}
;    {:args (7), :err #error {:cause "Divide by zero" ...}}]"

There are a couple of options to specify for the instrument macro:

  • :with-depth <bool>: Attaches :depth to each execution log
  • :xform <xform>: Enables transducer integration
  • :session <session>: Specifies the session to use

If you call instrument with the option {:with-depth true}, Postmortem automatically counts the current nesting level (depth) of function calls and attach it to each execution log:

(defn fact [n]
  (if (= n 0)
    1
    (* n (fact (dec n)))))

(pi/instrument `fact {:with-depth true})

(fact 5) ;=> 120
(pm/log-for `fact)
;=> [{:depth 1, :args (5)}
;    {:depth 2, :args (4)}
;    {:depth 3, :args (3)}
;    {:depth 4, :args (2)}
;    {:depth 5, :args (1)}
;    {:depth 6, :args (0)}
;    {:depth 6, :args (0), :ret 1}
;    {:depth 5, :args (1), :ret 1}
;    {:depth 4, :args (2), :ret 2}
;    {:depth 3, :args (3), :ret 6}
;    {:depth 2, :args (4), :ret 24}
;    {:depth 1, :args (5), :ret 120}]

Calling instrument with the option {:xform <xform>} enables integration of transducers with the instrumentation facility, passing the transducer <xform> to the underlying logging operators behind instrumentation. The example below shows how you can utilize a transducer to narrow down the execution log to the last few items until immediately before an error occurs:

(require '[postmortem.core :as pm]
         '[postmortem.instrument :as pi]
         '[postmortem.xforms :as xf])

(pm/reset!)

(pi/instrument `broken-factorial
               {:xform (comp (xf/take-until :err) (xf/take-last 5))})
;=> [user/broken-factorial]

(broken-factorial 7) ;; throws error
(pm/log-for `broken-factorial)
;=> [{:args (1), :ret 1}
;    {:args (2), :ret 2}
;    {:args (3), :ret 6}
;    {:args (4), :ret 24}
;    {:args (5), :err #error {:cause "Divide by zero" ...}}]

Also, you can even isolate the session for a function instrumentation by specifying the {:session <session>} option:

(def sess (pm/make-session))

(defn f [x] (inc x))

(pi/instrument `f {:session sess})

(dotimes [i 5] (prn (f i)))

(pm/log-for `f) ;=> nil
(pm/log-for sess `f)
;=> [{:args (0)}
;    {:args (0), :ret 1}
;    {:args (1)}
;    {:args (1), :ret 2}
;    {:args (2)}
;    {:args (2), :ret 3}
;    {:args (3)}
;    {:args (3), :ret 4}
;    {:args (4)}
;    {:args (4), :ret 5}]

Related works

  • scope-capture: Focuses on recreating a local environment at some point and seems rather intended to be a simple alternative for rich debuggers while Postmortem aims to compensate for some weakness in existing debuggers
  • miracle.save: Closely resembles Postmortem's basics, but Postmortem has more fancy features such as transducer integration and instrumentation

License

Copyright © 2018 Shogo Ohta

Distributed under the Eclipse Public License either version 1.0 or (at your option) any later version.

More Repositories

1

kitchen-async

A Promise library for ClojureScript, or a poor man's core.async
Clojure
162
star
2

japanese-clojure-companies

日本で Clojure/ClojureScript を利用している会社一覧
110
star
3

JiSE

JiSE: Java in S-Expression
Clojure
104
star
4

Pinpointer

Pinpointer is yet another clojure.spec error reporter based on a precise error analysis
Clojure
94
star
5

pogonos

Yet another Clojure(Script) implementation of the Mustache templating language
Clojure
74
star
6

sweet-array

Array manipulation library for Clojure with "sweet" array type notation and more safety by static types
Clojure
65
star
7

trenchman

A standalone nREPL/prepl client written in Go and heavily inspired by Grenchman
Go
63
star
8

power-dot

Clojure library for enhanced Java interop that helps you make friends with Java's functional interfaces 😍
Clojure
52
star
9

stacktracer

A small development utility to print stack trace with code context
Clojure
49
star
10

Drains

A new abstraction for flexible and efficient sequence aggregation in Clojure(Script)
Clojure
34
star
11

clj-check

lein-check alternative for Clojure CLI tool
Clojure
23
star
12

type-infer

A Clojure utility to inspect static types inferred by the Clojure compiler
Clojure
21
star
13

genman

Generator management utility for clojure.spec
Clojure
20
star
14

spectrace

clojure.spec (spec.alpha) library aiming to be a fundamental tool for analyzing spec errors
Clojure
16
star
15

prejenter

Presentation framework for Clojurians
Clojure
12
star
16

syntactic-closure

syntactic closures built on top of Clojure's macro system
Clojure
12
star
17

symbol-analyzer

Clojure code analyzer that tells us how each symbol is being used in the code
Clojure
11
star
18

Bigmouth

Clojure framework to build delivery-only Mastodon-compatible web apps
Clojure
11
star
19

lein-with-env-vars

A Leiningen plugin for performing a task with environment variable settings loaded from project.clj
Clojure
11
star
20

genuine-highlighter

a macro-aware syntax highlighter for Clojure
Clojure
10
star
21

admin-kit

Admin site as data
Clojure
9
star
22

aintegrant

Aintegrant ain't Integrant, it's Async Integrant!
Clojure
9
star
23

parabola

Hubot-like chatbot framework powered by Clojure/core.async
Clojure
7
star
24

tetrlang

an esoteric language inspired by Tetris
Clojure
7
star
25

emsh

An embedded shell built on top of Clojure
Clojure
7
star
26

intro-to-prejenter

Clojure
5
star
27

igv-client

Clojure implementation of IGV client to control IGV via its Port Commands
Clojure
4
star
28

iasm

inline assembly macros for Clojure
Clojure
4
star
29

mekki

Clojure-flavored Alloy language implemented on top of Clojure macro system
Clojure
4
star
30

maeni

A toy implementation of the programming language Forth written in Clojure
Clojure
4
star
31

clojars-rss

Unofficially generated RSS feeds for libraries recently updated on Clojars
Clojure
3
star
32

reading-clojurescript

A project for reading ClojureScript source code
Clojure
3
star
33

SimpleLisp

a simple Lisp implementation in Java as a study
Java
3
star
34

AR-sample

AR sample project using AR.js from ClojureScript
Clojure
3
star
35

reading-clojure

A project for reading Clojure's source code
Java
3
star
36

nrepl-revolver

Docker-backed nREPL server aiming for secure execution and fast startup
Clojure
2
star
37

applicosan

My personal Slack bot built with Duct framework
Clojure
2
star
38

clj-skkserv

An skkserv server framework in Clojure, heavily inspired by Ring architecture
Clojure
2
star
39

white-scheme

an implementation of Scheme-to-Whitespace compiler
Scheme
2
star
40

pwa-in-cljs

Clojure
2
star
41

code-marker

A versatile code marker for Clojure
Clojure
2
star
42

clj-wiki

a wiki engine written in Clojure
Clojure
2
star
43

athos.github.io

https://athos.github.io/
HTML
2
star
44

advent-of-code-2018

My solutions for Advent of Code 2018 http://adventofcode.com/2018
Clojure
2
star
45

land-of-clojurescript

A ClojureScript port of the code from "Land of Lisp", mainly for the "Dice of Doom" game
Clojure
2
star
46

OCheme

OCheme is a toy implementation of Scheme written in OCaml.
OCaml
2
star
47

twtr2mvc

A Twitter Mixi-Voice bridge
Clojure
2
star
48

silly

A silly personal assistant for demonstrating the combination of Web Speech API and Microsoft LUIS
Clojure
2
star
49

spec-example

Clojure
2
star
50

c-compijer

Clojure
2
star
51

gauchey

A Clojure library ported from Gauche's bundled library.
Clojure
2
star
52

rusp

A SECD-based Lisp implementation in Rust
Rust
2
star
53

task-jockey

Pueue-like task management tool implemented in Clojure
Clojure
2
star
54

abstract-machine-collection

A collection of abstract machine implementations
Clojure
1
star
55

mario-brojures

A port implementation of MariOCaml in ClojureScript
Clojure
1
star
56

bfmaker

Clojure
1
star
57

clj-callgraph

A call graph generator for Clojure (with some fun features)
Clojure
1
star
58

Lustered

This project has been moved to https://github.com/athos/admin-kit.
Clojure
1
star
59

experimental-core-specs

An experimental project aimed at investigating how it would be once core.specs were fully introduced
Clojure
1
star
60

as2twtr

AsakusaSatellite Twitter Bridge
Clojure
1
star
61

specium

Inverse of s/form, reducing eval calls as much as possible
Clojure
1
star
62

Project-Euler

athos' solutions for problems of Project Euler
Clojure
1
star
63

kodamacast

Clojure
1
star
64

lein-highlight

Leiningen plugin for highlighting Clojure code powered by symbol-analyzer, a fine-grained Clojure code analyzer
Clojure
1
star
65

TIL

Today I Learned
1
star
66

alexa-skill-sample

Clojure
1
star
67

goclojure

Brings goruby-like abbreviation to Clojure
Clojure
1
star