Utilities to track and monitor queries made by toucan2. Usage: (start!) ;; start tracking ;; do some query using toucan 2 or via UI (queries) ;; get all queries and its execution time ;; => [[["SELECT * FROM report_card"] 100]] (stop!) (to-csv!) ;; to save all queries to a csv file | (ns dev.toucan2-monitor (:require [clojure.data.csv :as csv] [clojure.java.io :as io] [clojure.stacktrace :as stacktrace] [clojure.string :as str] [dev.util :as dev.u] [metabase.test.util.log :as tu.log] [metabase.util :as u] [metabase.util.log :as log] [methodical.core :as methodical] [toucan2.pipeline :as t2.pipeline]) (:import (java.io File))) |
(set! *warn-on-reflection* true) | |
An atom to store all the queries and its execution time. | (def queries* (atom [])) |
Get all the queries and its execution time in ms. Usage: (queries) ;; => [[["SELECT * FROM report_card"] 100]] | (defn queries [] @queries*) |
Reset all the queries and its execution time. | (defn reset-queries! [] (reset! queries* [])) |
Get the total number of queries and total execution time in ms. | (defn summary
[]
(let [qs (queries)]
{:total-queries (count qs)
:total-execution-time-ms (->> qs (map second) (apply +) int)})) |
Return first callsite inside Metabase that's not | (defn call-site
[]
(let [trace (->> (with-out-str
(stacktrace/print-stack-trace (Exception. "tracker")))
str/split-lines)]
(some-> (u/seek #(and (re-find #"^\s*metabase\." %)
(not (re-find #"^\s*metabase\.db" %))) trace)
str/trim))) |
(defn- track-query-execution-fn
[next-method rf conn query-type model query]
(let [start (System/nanoTime)
result (next-method rf conn query-type model query)
end (System/nanoTime)]
(swap! queries* (fnil conj []) [query (/ (- end start) 1e6) (call-site)])
result)) | |
(def ^:private log-thread-ref (volatile! nil)) | |
(defn- create-log-thread! []
(Thread.
(fn []
(while (not (Thread/interrupted))
(let [{:keys [total-queries total-execution-time-ms]} (summary)]
(log/infof "Total queries: %d, Total execution time: %dms" total-queries total-execution-time-ms)
(Thread/sleep 1000)))))) | |
(defn- start-log! []
(tu.log/set-ns-log-level! *ns* :debug)
(when-not (some? @log-thread-ref)
(let [new-thread (create-log-thread!)]
(vreset! log-thread-ref new-thread)
(.start ^Thread new-thread)))) | |
(defn- stop-log! []
(when-let [thread @log-thread-ref]
(.interrupt ^Thread thread)
(vreset! log-thread-ref nil))) | |
Start tracking queries. | (defn start! [] (stop-log!) (start-log!) (methodical/add-aux-method-with-unique-key! #'t2.pipeline/transduce-execute-with-connection :around :default track-query-execution-fn ::monitor)) |
Stop tracking queries. | (defn stop! [] (stop-log!) (methodical/remove-aux-method-with-unique-key! #'t2.pipeline/transduce-execute-with-connection :around :default ::monitor)) |
Save all the queries and its execution time to a csv file with 3 columns: query, params, execution time. | (defn to-csv!
[]
(let [qs (queries)
format-q (fn [[q t]]
[(-> q first #_mdb.query/format-sql) (-> q rest vec) t])
temp-file (File/createTempFile "queries" ".csv")]
(with-open [w (io/writer temp-file)]
(csv/write-csv w (cons ["query" "params" "execution-time(ms)"] (map format-q qs))))
(dev.u/os-open temp-file))) |
Implementation for [[with-queries]]. | (defn do-with-queries
[f]
(reset-queries!)
(start!)
(u/prog1 (f queries)
(stop!))) |
See Toucan queries executed: ```clj (with-queries [queries] (select ...) (println :total (count (queries)))) ;; -> :total 1 ``` | (defmacro with-queries [[queries-binding] & body] `(do-with-queries (^:once fn* [~queries-binding] ~@body))) |
(comment
(start!)
(queries)
(stop!)
(reset-queries!)
(summary)
(to-csv!)
(doseq [q (querles)]
#_:clj-kondo/ignore
(println q))) | |