Skip to content

Using a logging library

Brian Marick edited this page Sep 30, 2015 · 9 revisions

(The examples below can be found in the examples directory.)

This library's default behavior is to print explanations of type errors to standard output via println. That's pretty bare-bones. You may want to use a logging library of some sort. This page shows how to integrate Timbre.

The checked function produces a collection of oopsies. Each of them is about a particular predicate's reason for returning a falsey value. That collection is passed to a error handler that (typically) converts the oopsies into explanations and then does something with them.

You override the default error handler with replace-error-handler or, if you are using the implicit global type repo, on-error!.

So when following the recommended setup, println can be replaced with Timbre's error like this:

(ns my.types
  (:require [structural-typing.type :as type]
            [structural-typing.assist.oopsie :as oopsie]
            [taoensso.timbre :as timbre])
  ...)

(def type-repo
  (-> empty-type-repo
      ...
      (replace-error-handler
       (oopsie/mkfn:apply-to-each-explanation #(timbre/error %)))))

(error is wrapped in a function because it's actually a macro, and I was too lazy to find out what function its expansion eventually calls.)

That having been done, you get errors with more detail:

user=> (require '[my.types :as type])
user=> (type/built-like :Point {:x "1"})
15-Jun-29 12:38:01 busted-2.local ERROR [timbre-define-1] - :x should be `integer?`; it is `"1"`
15-Jun-29 12:38:01 busted-2.local ERROR [timbre-define-1] - :y must exist and be non-nil
nil

Notice that the return value is still nil, which mkfn:apply-to-each-explanation guarantees.


When I look at logs, I like to see both the entire value that provoked the error and also a stack trace that tells me where in the code it happened. To satisfy me, we'll do the following for any type error:

  • Print the original value at the info level.
  • Print each explanation, also at the info level.
  • Print the stack trace at the error level (which my own variant of Timbre's error does automatically).

The code looks like this:

(timbre/set-level! :info)

(def pprint-to-string #(with-out-str pprint %))

(defn error-explainer [oopsies]
  (timbre/info "While checking this:")
  (-> (first oopsies) ; the error handler is always given at least one oopsie.
      :whole-value    ; the original candidate being checked
      pprint-to-string
      str/trimr       ; be tidy by getting rid of pprint's trailing newline
      timbre/info)
  (doseq [e (oopsie/explanations oopsies)] (timbre/info e))
  (timbre/error "Boundary type check failed"))

(def type-repo
  (-> empty-type-repo
      ...
      (replace-error-handler error-explainer)))

Notice that explanations is used to produce the text appropriate to each oopsie's predicate.

The result of the above is this:

user=> (type/checked :Point {:x "1"})
15-Jun-29 12:51:18 busted-2.local INFO [timbre-define-2] - While checking this:
15-Jun-29 12:51:18 busted-2.local INFO [timbre-define-2] - {:x "1"}
15-Jun-29 12:51:18 busted-2.local INFO [timbre-define-2] - :x should be `integer?`; it is `"1"`
15-Jun-29 12:51:18 busted-2.local INFO [timbre-define-2] - :y must exist and be non-nil
15-Jun-29 12:51:18 busted-2.local ERROR [timbre-define-2] - Boundary type check failed
[stack trace omitted]