Skip to content

Commit 930826a

Browse files
authored
Merge pull request #120 from clojureverse/oxalorg/feature-db-stats
Message volume statistics
2 parents 2715507 + a5ad9f3 commit 930826a

File tree

6 files changed

+196
-2
lines changed

6 files changed

+196
-2
lines changed

repl/stats_from_indexer.clj

Lines changed: 84 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,84 @@
1+
(ns repl-sessions.stats-from-indexer
2+
(:require [clojurians-log.time-util :as time-util])
3+
(:import (java.time LocalDate)))
4+
5+
;; Fetching all messages for a given channel and day is really fast, but we also
6+
;; want to know the list of all channels for a given day, and how many messages
7+
;; each had, and which day was the previous or next day that had messages, and
8+
;; these queries are really slow, since they basically need to traverse all
9+
;; messages.
10+
11+
;; To work around that we build up these "indexes" and keep them in an atom. We
12+
;; have an "indexer" component that rebuilds them regularly.
13+
14+
(keys @clojurians-log.db.queries/!indexes)
15+
;; => (:chan-day-cnt :day-chan-cnt :chan-id->name :chan-name->id)
16+
17+
(defn day-chan-cnt []
18+
(:day-chan-cnt @clojurians-log.db.queries/!indexes))
19+
20+
;; For example day-chan-cnt groups first on day, then on channel, and then shows
21+
;; the count of messages.
22+
23+
(day-chan-cnt)
24+
;;=>
25+
{"2018-01-28" {"C03S1KBA2" 1},
26+
"2018-02-02" {"C064BA6G2" 28,
27+
"C099W16KZ" 19,
28+
"C0617A8PQ" 51,
29+
,,,}}
30+
31+
;; So we can easily sum up all messages for a given day.
32+
33+
(defn day-total [day]
34+
(apply + (vals (get (day-chan-cnt) day))))
35+
36+
(day-total "2018-02-02")
37+
;; => 887
38+
39+
;; If we want to query this we just need to extrapolate to a range of days. We
40+
;; use clojure.java-time elsewhere but actually the java.time API is quite nice
41+
;; and nowadays I tend to use it directly. A simple date (so year+month+day)
42+
;; without any time or timezone information is represented as a
43+
;; java.time.LocalDate.
44+
45+
;; Good example here of recursion and lazy-seq. Note that the lazy-seq is
46+
;; optional here, you can remove it and still get a valid result, it would just
47+
;; be eager instead of lazy.
48+
49+
;; There are obviously more ways to write this, for instance with loop/recur.
50+
;; This use of recursion + cons is a very "classic lisp" approach.
51+
52+
(defn range-of-local-dates [^LocalDate ld1 ^LocalDate ld2]
53+
(when (.isBefore ld1 ld2)
54+
(cons ld1 (lazy-seq (range-of-local-dates (.plusDays ld1 1) ld2)))))
55+
56+
;; A bit more clojure-y, use vectors with [year month day], return strings
57+
;; like "2018-02-02", since that is what we have in the indexes. Note that just
58+
;; calling `str` on any java.time class usually gives a nicely formatted result.
59+
60+
(defn range-of-days [[y1 m1 d1] [y2 m2 d2]]
61+
(map str
62+
(range-of-local-dates
63+
(java.time.LocalDate/of y1 m1 d1)
64+
(java.time.LocalDate/of y2 m2 d2))))
65+
66+
;; So this is what that looks like now. I made the range half-open (not
67+
;; including the end date), might make more sense to make it inclusive.
68+
69+
(range-of-days [2018 2 2] [2018 2 5])
70+
;; => ("2018-02-02" "2018-02-03" "2018-02-04")
71+
72+
;; So now we can grab the numbers for these days and sum them up. This is a
73+
;; textbook example of where a transducer works nicely. Check the LI episode for
74+
;; transducers if you haven't seen this before. Obviously there are other ways
75+
;; to write this too, like a simple (apply + (map ...))
76+
77+
(defn days-total [days]
78+
(transduce (map day-total) + 0 days))
79+
80+
;; And there you go
81+
82+
(days-total
83+
(range-of-days [2018 2 2] [2018 2 5]))
84+
;; => 1913

resources/public/js/stats.js

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
var ctx = document.getElementById('message-chart');
2+
3+
var messageJson = document.getElementById('message-data').innerHTML;
4+
var messageData = JSON.parse(messageJson.replace(/"/g,'"'));
5+
var labels = messageData.map(function(elem) {
6+
return elem["day"];
7+
})
8+
var data = messageData.map(function(elem) {
9+
return elem["msg-count"];
10+
})
11+
12+
var messageChart = new Chart(ctx, {
13+
type: 'line',
14+
data: {
15+
labels: labels,
16+
datasets: [{
17+
label: 'No. of slack messages',
18+
fill: false,
19+
data: data,
20+
backgroundColor: 'rgba(255, 99, 132, 0.2)',
21+
borderColor: 'rgba(54, 162, 235, 1)',
22+
borderWidth: 2
23+
}]
24+
},
25+
});

src/clojurians_log/db/queries.clj

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -137,6 +137,24 @@
137137
[?emoji :emoji/url ?url]]
138138
db)))
139139

140+
(defn unique-users-between-days [db from-day to-day]
141+
(d/q '[:find (count ?user)
142+
:in $ ?from-day ?to-day
143+
:where
144+
[?msg :message/user ?user]
145+
[?msg :message/day ?day]
146+
[(>= ?day ?from-day)]
147+
[(<= ?day ?to-day)]]
148+
db
149+
from-day
150+
to-day))
151+
152+
(defn message-stats-between-days [from-day to-day]
153+
(letfn [(day-chan-cnt [] (:day-chan-cnt @!indexes))
154+
(day-total [day] (apply + (vals (get (day-chan-cnt) day))))
155+
(days-total [days] (transduce (map day-total) + 0 days))]
156+
(mapv #(hash-map :day % :msg-count (day-total %)) (time-util/range-of-days from-day to-day))))
157+
140158
#_
141159
(doseq [v [#'clojurians-log.db.queries/user-names
142160
#'clojurians-log.db.queries/channel

src/clojurians_log/routes.clj

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -144,6 +144,15 @@
144144
views/sitemap
145145
response/xml-render)))
146146

147+
(defn message-stats-route [{:keys [endpoint] :as request}]
148+
(let [config @(get-in endpoint [:config :value])
149+
{:keys [from-date to-date]} (:path-params request)]
150+
(-> request
151+
make-context
152+
(assoc :data/message-stats (queries/message-stats-between-days from-date to-date))
153+
views/message-stats-page
154+
response/render)))
155+
147156
(def routes
148157
[["/" {:name :clojurians-log.routes/index
149158
:get index-route}]
@@ -158,4 +167,5 @@
158167
["/{channel}/{date}" {:name :clojurians-log.routes/channel-date,
159168
:get log-route}]
160169
["/{channel}/{date}/{ts}" {:name :clojurians-log.routes/message,
161-
:get log-route}]])
170+
:get log-route}]
171+
["/_/stats/{from-date}/{to-date}" {:name :clojurians-log.routes/message-stats :get message-stats-route}]])

src/clojurians_log/time_util.clj

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22
(:require [java-time :as jt]
33
[java-time.local :as jt.l]
44
[clojure.string :as str])
5-
(:import [java.time Instant]
5+
(:import [java.time Instant LocalDate]
66
[java.time.format DateTimeFormatter]))
77

88
(defn ts->inst
@@ -81,3 +81,14 @@
8181
ret# ~expr]
8282
(prn (str ~label ": " (/ (double (- (. System (nanoTime)) start#)) 1000000.0) " msecs"))
8383
ret#))
84+
85+
(defn range-of-local-dates [^LocalDate ld1 ^LocalDate ld2]
86+
(when (.isBefore ld1 (.plusDays ld2 1))
87+
(cons ld1 (lazy-seq (range-of-local-dates (.plusDays ld1 1) ld2)))))
88+
89+
(defn range-of-days
90+
"Takes 2 day values as a strings and returns range of all days between them inclusive of both"
91+
[from-day to-day]
92+
(let [ld1 (java.time.LocalDate/parse from-day)
93+
ld2 (java.time.LocalDate/parse to-day)]
94+
(map str (range-of-local-dates ld1 ld2))))

src/clojurians_log/views.clj

Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22
(:require [hiccup2.core :as hiccup]
33
[cemerick.url :refer [url]]
44
[clojurians-log.time-util :as cl.tu]
5+
[clojure.data.json :as json]
56
[clojure.string :as str]
67
[clojurians-log.slack-messages :as slack-messages]
78
[reitit.core]
@@ -16,6 +17,11 @@
1617
[messages ts]
1718
(some #(when (= (:message/ts %) ts) %) messages))
1819

20+
(defn jsfile [path]
21+
(when-let [f (io/file (io/resource (str "public" path)))]
22+
(let [ts (.lastModified f)]
23+
[:script
24+
{:src (str path "?version=" ts)}])))
1925

2026
(defn stylesheet [path]
2127
(when-let [f (io/file (io/resource (str "public" path)))]
@@ -286,6 +292,43 @@
286292
:date day}))]
287293
[:lastmod day]]))])
288294

295+
296+
(defn- page-head-stats [{:data/keys [title]}]
297+
[:head
298+
[:meta {:charset "utf-8"}]
299+
[:meta {:http-equiv "X-UA-Compatible" :content "IE=edge"}]
300+
[:meta {:content="width=device-width, initial-scale=1" :name "viewport"}]
301+
[:title title]
302+
[:link {:href "https://unpkg.com/sakura.css/css/sakura.css"
303+
:rel "stylesheet"
304+
:type "text/css"}]
305+
(stylesheet "/css/gh-fork-ribbon.min.css")
306+
[:script {:src "https://cdnjs.cloudflare.com/ajax/libs/Chart.js/2.9.4/Chart.min.js"}]
307+
])
308+
309+
(defn- message-stats-page-html [{:data/keys [message-stats] :as context}]
310+
[:html
311+
(page-head-stats context)
312+
[:body
313+
[:div
314+
[:h4 "Slack message stats"]
315+
[:p
316+
[:strong {:style {:border-bottom "1px solid black"}} (:day (first message-stats))]
317+
" to "
318+
[:strong {:style {:border-bottom "1px solid black"}} (:day (last message-stats))]]
319+
[:div {:width "100%"}
320+
[:canvas#message-chart]]
321+
[:h4 "Total message count: " (reduce #(+ %1 (:msg-count %2)) 0 message-stats)]
322+
[:table
323+
[:thead
324+
[:tr
325+
[:th "Day"]
326+
[:th "Message count"]]]
327+
[:tbody
328+
(for [day-stat message-stats] [:tr [:td (:day day-stat)] [:td (:msg-count day-stat)]])]]]
329+
[:script#message-data {:type "application/json"} (json/write-str message-stats)]
330+
(jsfile "/js/stats.js")]])
331+
289332
(defn log-page [context]
290333
(assoc context :response/html (log-page-html context)))
291334

@@ -300,3 +343,6 @@
300343

301344
(defn sitemap [context]
302345
(assoc context :response/xml (sitemap-xml context)))
346+
347+
(defn message-stats-page [context]
348+
(assoc context :response/html (message-stats-page-html context)))

0 commit comments

Comments
 (0)