oakes / odoyle-rules

A rules engine for Clojure(Script)
The Unlicense
530 stars 20 forks source link

Ideas for improving debugging #19

Closed oakes closed 1 year ago

oakes commented 1 year ago

After @thomascothran brought it up in #18 i figured we should have a high level issue for improving debugging in odoyle. I think there are several things that would be nice to have:

  1. A way to see a "trace" of all rule firings, or potentially a subset of them, similar to the tracing functionality in Clara.
  2. A way to see why a rule fired (i.e., what specific tuples in the :what block triggered it)
  3. A way to see why a rule didn't fire (possibly the most difficult thing of all!)

One simple technique i've used to get a primitive form of step debugging while a rule fires is just to use read-line in my :then or :then-finally blocks. For example, in a rule, you could just put something like (println :my-rule-name o/*match*) (read-line) and it will print the data out and pause execution there until you hit enter.

Additionally, if you want to get an entire trace of rule firings, it is fairly easy to implement. For example, here's a function i wrote a while ago:

(defn wrap-rule                
  "Wraps the functions of a rule so they can be conveniently intercepted
  for debugging or other purposes." 
  [rule {when-fn :when, then-fn :then, then-finally-fn :then-finally}]
  (cond-> rule
          (and (:when-fn rule) when-fn)
          (update :when-fn 
                  (fn wrap-when [f]
                    (fn [match]
                      (when-fn f match))))
          (and (:then-fn rule) then-fn)
          (update :then-fn
                  (fn wrap-then [f]
                    (fn [match]
                      (then-fn f match))))
          (and (:then-finally-fn rule) then-finally-fn)
          (update :then-finally-fn
                  (fn wrap-then-finally [f]
                    (fn []
                      (then-finally-fn f))))))

With this, you can build the tracing data structure yourself:

(def *steps (atom [])) 

(->> rules
     (map (fn [rule]
            (wrap-rule rule 
                       {:when
                        (fn [f match]
                          (swap! *steps conj [:when (:name rule) match])
                          (f match))
                        :then
                        (fn [f match]
                          (swap! *steps conj [:then (:name rule) match])
                          (f match))
                        :then-finally
                        (fn [f]
                          (swap! *steps conj [:then-finally (:name rule)])
                          (f))})))
     (reduce o/add-rule (o/->session)))

Of course, you can limit it to only certain rules if you wish. Is it worth adding to the library itself? Maybe, but i haven't done so yet because i'm not sure if it's the best design. Feedback welcome.

The second and third points above would definitely require built-in support. Right now i'm not keeping track of which tuples caused a rule to trigger, but i don't think it'd be that hard to add. I need to think more about how to actually surface that functionality, and maybe provide a way to turn it off if it adds too much perf cost. I'll add more to this issue after thinking about it more.

oakes commented 1 year ago

Closing this because i just released 1.0 with most of the above ideas included:

https://github.com/oakes/odoyle-rules/releases/tag/1.0.0