diff options
| author | Felix Hanley <felix@userspace.com.au> | 2020-03-18 13:25:09 +0000 |
|---|---|---|
| committer | Felix Hanley <felix@userspace.com.au> | 2020-03-18 13:25:09 +0000 |
| commit | ac6468f9eac688a2fb379ea332fd45490cf80cf4 (patch) | |
| tree | 581fe758fb471e22cf1b1ef85726bc4064e510a8 | |
| parent | 2248b4d7e1d083a103e94985ee4b373d689ae0e8 (diff) | |
| download | sws-ac6468f9eac688a2fb379ea332fd45490cf80cf4.tar.gz sws-ac6468f9eac688a2fb379ea332fd45490cf80cf4.tar.bz2 | |
Unify chart handler, add pie charts, better caching
| -rw-r--r-- | browser_set.go | 42 | ||||
| -rw-r--r-- | charts.go | 49 | ||||
| -rw-r--r-- | cmd/server/charts.go | 127 | ||||
| -rw-r--r-- | cmd/server/helpers.go | 14 | ||||
| -rw-r--r-- | cmd/server/hits.go | 3 | ||||
| -rw-r--r-- | cmd/server/routes.go | 11 | ||||
| -rw-r--r-- | hit_set.go | 21 | ||||
| -rw-r--r-- | store/sqlite3.go | 4 | ||||
| -rw-r--r-- | tmpl/site.tmpl | 1 |
9 files changed, 178 insertions, 94 deletions
diff --git a/browser_set.go b/browser_set.go index 4d3107c..c4b9865 100644 --- a/browser_set.go +++ b/browser_set.go @@ -1,6 +1,7 @@ package sws import ( + "sort" "time" detector "github.com/mssola/user_agent" @@ -54,6 +55,17 @@ func NewBrowserSet(hs *HitSet) BrowserSet { } return BrowserSet(out) } +func (bs *BrowserSet) SortByName() { + sort.Slice(*bs, func(i, j int) bool { + return (*bs)[i].Label() < (*bs)[j].Label() + }) +} + +func (bs *BrowserSet) SortByHits() { + sort.Slice(*bs, func(i, j int) bool { + return (*bs)[i].hitSet.Count() > (*bs)[j].hitSet.Count() + }) +} func (b Browser) Label() string { return b.Name @@ -66,7 +78,37 @@ func (b Browser) Count() int { func (bs BrowserSet) Count() int { return len(bs) } +func (bs BrowserSet) Labels() []string { + out := make([]string, len(bs)) + for i := 0; i < len(bs); i++ { + out[i] = bs[i].Label() + } + return out +} +func (bs BrowserSet) Counts() []int { + out := make([]int, len(bs)) + for i := 0; i < len(bs); i++ { + out[i] = bs[i].Count() + } + return out +} +/* +func (bs BrowserSet) Ratios() []float64 { + out := make([]float64, len(bs)) + max := 0.0 + for i := 0; i < len(bs); i++ { + out[i] = float64(bs[i].Count()) + if out[i] > max { + max = out[i] + } + } + for i := 0; i < len(out); i++ { + out[i] = out[i] / max + } + return out +} +*/ func (bs BrowserSet) YMax() int { max := 0 for _, b := range bs { @@ -9,27 +9,23 @@ import ( "github.com/wcharczuk/go-chart/drawing" ) -type Chartable interface { - YMax() int - XSeries() []Countable +type counted interface { + Counts() []int } - -type Countable interface { - Label() string - Count() int +type labelled interface { + Labels() []string } - -/* -type TimeChartable interface { - XMax() int - Series() []TimeCountable +type timed interface { + Times() []time.Time } - -type TimeCountable interface { - XValue() time.Time - YValue() int +type timeCounted interface { + counted + timed +} +type labelCounted interface { + counted + labelled } -*/ type chart struct { width, height int @@ -181,3 +177,22 @@ func HitChartSVG(w io.Writer, data *HitSet, d time.Duration) error { graph.Render(gochart.SVG, w) return nil } + +func PieChartSVG(w io.Writer, data labelCounted) error { + labels := data.Labels() + counts := data.Counts() + values := make([]gochart.Value, len(labels)) + for i := 0; i < len(labels); i++ { + values[i] = gochart.Value{ + Value: float64(counts[i]), + Label: labels[i], + } + } + pie := gochart.PieChart{ + Width: 400, + Height: 400, + Values: values, + } + pie.Render(gochart.SVG, w) + return nil +} diff --git a/cmd/server/charts.go b/cmd/server/charts.go index 875b528..fce5c86 100644 --- a/cmd/server/charts.go +++ b/cmd/server/charts.go @@ -1,15 +1,18 @@ package main import ( + "crypto/sha1" + "fmt" "net/http" "strconv" + "strings" "time" "github.com/go-chi/chi" "src.userspace.com.au/sws" ) -func sparklineHandler(db sws.HitStore) http.HandlerFunc { +func chartHandler(db sws.HitStore) http.HandlerFunc { return func(w http.ResponseWriter, r *http.Request) { ctx := r.Context() site, ok := ctx.Value("site").(*sws.Site) @@ -19,20 +22,34 @@ func sparklineHandler(db sws.HitStore) http.HandlerFunc { return } - beginSecs, err := strconv.ParseInt(chi.URLParam(r, "b"), 10, 64) + chartType := chi.URLParam(r, "type") + dataType := chi.URLParam(r, "data") + beginSecs, err := strconv.ParseInt(chi.URLParam(r, "begin"), 10, 64) if err != nil { - log(err) - http.Error(w, http.StatusText(404), 404) + httpError(w, http.StatusNotFound, err.Error()) return } - endSecs, err := strconv.ParseInt(chi.URLParam(r, "e"), 10, 64) + endSecs, err := strconv.ParseInt(chi.URLParam(r, "end"), 10, 64) if err != nil { - log(err) - http.Error(w, http.StatusText(404), 404) + httpError(w, http.StatusNotFound, err.Error()) return } begin := time.Unix(beginSecs, 0) end := time.Unix(endSecs, 0) + + var b strings.Builder + b.WriteString(r.URL.Path) + // FIXME + b.WriteString(time.Now().Truncate(30 * time.Minute).String()) + etag := fmt.Sprintf(`"%x"`, sha1.Sum([]byte(b.String()))) + + if match := r.Header.Get("If-None-Match"); match != "" { + if strings.Contains(match, etag) { + w.WriteHeader(http.StatusNotModified) + return + } + } + filter := map[string]interface{}{ "begin": begin, "end": end, @@ -40,76 +57,56 @@ func sparklineHandler(db sws.HitStore) http.HandlerFunc { hits, err := db.GetHits(*site, filter) if err != nil { - log(err) - http.Error(w, http.StatusText(404), 404) - return - } - debug("retrieved", len(hits), "hits") - data, err := sws.NewHitSet(sws.FromHits(hits)) - if err != nil { - log(err) - http.Error(w, http.StatusText(500), 500) + httpError(w, http.StatusInternalServerError, err.Error()) return } - // Ensure the buckets start and end at the right time - data.Fill(&begin, &end) - w.Header().Set("Content-Type", "image/svg+xml") - w.Header().Set("Cache-Control", "public") - sws.SparklineSVG(w, data, time.Hour) - } -} - -func svgChartHandler(db sws.HitStore) http.HandlerFunc { - return func(w http.ResponseWriter, r *http.Request) { - beginSecs, err := strconv.ParseInt(chi.URLParam(r, "b"), 10, 64) + hitSet, err := sws.NewHitSet(sws.FromHits(hits)) if err != nil { - log(err) - http.Error(w, http.StatusText(404), 404) + httpError(w, http.StatusInternalServerError, err.Error()) return } - endSecs, err := strconv.ParseInt(chi.URLParam(r, "e"), 10, 64) - if err != nil { - log(err) - http.Error(w, http.StatusText(404), 404) - return - } - begin := time.Unix(beginSecs, 0) - end := time.Unix(endSecs, 0) - - ctx := r.Context() - site, ok := ctx.Value("site").(*sws.Site) - if !ok { - http.Error(w, http.StatusText(422), 422) + if hitSet == nil { + httpError(w, http.StatusInternalServerError, "missing hitset") return } - // width := 400 - // height := 200 - // if w := q.Get("width"); w != "" { - // width, _ = strconv.Atoi(w) - // } - // if h := q.Get("height"); h != "" { - // height, _ = strconv.Atoi(h) - // } + hitSet.Fill(&begin, &end) + hitSet.SortByDate() - hits, err := db.GetHits(*site, map[string]interface{}{ - "begin": begin, "end": end, - }) - if err != nil { - panic(err) - } - debug("retrieved", len(hits), "hits") + w.Header().Set("Etag", etag) + w.Header().Set("Cache-Control", "no-cache") - data, err := sws.NewHitSet(sws.FromHits(hits)) - if err != nil { - log(err) - http.Error(w, http.StatusText(500), 500) + switch dataType { + case "h": + w.Header().Set("Content-Type", "image/svg+xml") + switch chartType { + case "b": + sws.HitChartSVG(w, hitSet, time.Minute) + case "s": + sws.SparklineSVG(w, hitSet, time.Hour) + } + case "p": + pages := sws.NewBrowserSet(hitSet) + pages.SortByHits() + w.Header().Set("Content-Type", "image/svg+xml") + switch chartType { + case "p": + sws.PieChartSVG(w, pages) + } + case "b": + browsers := sws.NewBrowserSet(hitSet) + browsers.SortByHits() + w.Header().Set("Content-Type", "image/svg+xml") + switch chartType { + case "p": + sws.PieChartSVG(w, browsers) + } + case "c": + default: + log("invalid chart data type:", dataType) + http.Error(w, http.StatusText(http.StatusBadRequest), http.StatusBadRequest) return } - data.Fill(&begin, &end) - - w.Header().Set("Content-Type", "image/svg+xml") - sws.HitChartSVG(w, data, time.Minute) } } diff --git a/cmd/server/helpers.go b/cmd/server/helpers.go index 6c498e5..17e519f 100644 --- a/cmd/server/helpers.go +++ b/cmd/server/helpers.go @@ -10,14 +10,13 @@ import ( ) var funcMap = template.FuncMap{ + "piechart": func(siteID int, dataType string, begin, end time.Time) string { + return fmt.Sprintf("/sites/%d/charts/p-%s-%d-%d.svg", siteID, dataType, begin.Unix(), end.Unix()) + }, "sparkline": func(id int) string { - // This will enable "caching" for an hour - now := time.Now() //.Truncate(time.Hour) - //then := now.Add(-720 * time.Hour) + now := time.Now().Truncate(time.Hour) then := now.Add(-168 * time.Hour) // 7 days - //then := now.Add(-24 * time.Hour) - //then := now.Add(-1 * time.Hour) - return fmt.Sprintf("/sites/%d/sparklines/%d-%d.svg", id, then.Unix(), now.Unix()) + return fmt.Sprintf("/sites/%d/charts/s-h-%d-%d.svg", id, then.Unix(), now.Unix()) }, "tz": func(s string, t time.Time) time.Time { tz, _ := time.LoadLocation(s) @@ -76,8 +75,9 @@ func httpError(w http.ResponseWriter, code int, msg string) { } func extractTimeRange(r *http.Request) (*time.Time, *time.Time) { + // Default to 1 week ago begin := timePtr(time.Now().Truncate(time.Hour).Add(-168 * time.Hour)) - end := timePtr(time.Now().Truncate(time.Hour)) + end := timePtr(time.Now().Truncate(30 * time.Minute)) q := r.URL.Query() if b := q.Get("begin"); b != "" { if bs, err := strconv.ParseInt(b, 10, 64); err == nil { diff --git a/cmd/server/hits.go b/cmd/server/hits.go index e5aaf73..3a76bea 100644 --- a/cmd/server/hits.go +++ b/cmd/server/hits.go @@ -84,6 +84,9 @@ func handleHitCounter(db sws.CounterStore, mmdbPath string) http.HandlerFunc { //http.Error(w, err.Error(), http.StatusInternalServerError) //return } + + // TODO restrict to site sites + w.Header().Set("Access-Control-Allow-Origin", "*") w.Header().Set("Content-Type", "image/gif") w.Write(gifBytes) return diff --git a/cmd/server/routes.go b/cmd/server/routes.go index 5b93713..b9979fe 100644 --- a/cmd/server/routes.go +++ b/cmd/server/routes.go @@ -65,7 +65,10 @@ func createRouter(db sws.Store, mmdbPath string) (chi.Router, error) { // For counter r.Get("/sws.js", handleCounter(addr)) - r.Get("/sws.gif", handleHitCounter(db, mmdbPath)) + r.Group(func(r chi.Router) { + r.Use(middleware.NoCache) + r.Get("/sws.gif", handleHitCounter(db, mmdbPath)) + }) //r.Get("/hits", handleHits(db)) r.Group(func(r chi.Router) { @@ -134,11 +137,9 @@ func createRouter(db sws.Store, mmdbPath string) (chi.Router, error) { r.Get("/", siteHandler) r.Post("/", siteHandler) r.Get("/edit", handleSiteEdit(db, rndr)) - r.Route("/sparklines", func(r chi.Router) { - r.Get("/{b:\\d+}-{e:\\d+}.svg", sparklineHandler(db)) - }) + r.Route("/charts", func(r chi.Router) { - r.Get("/{b:\\d+}-{e:\\d+}.svg", svgChartHandler(db)) + r.Get("/{type:(p|s|b)}-{data:(h|b|c)}-{begin:\\d+}-{end:\\d+}.svg", chartHandler(db)) //r.Get("/{b:\\d+}-{e:\\d+}.png", svgChartHandler(db)) }) }) @@ -239,6 +239,27 @@ func (hs HitSet) XYValues() ([]time.Time, []float64) { } return x, y } +func (hs HitSet) Labels() []string { + out := make([]string, len(hs.buckets)) + for i := 0; i < len(hs.buckets); i++ { + out[i] = hs.buckets[i].Label() + } + return out +} +func (hs HitSet) Counts() []int { + out := make([]int, len(hs.buckets)) + for i := 0; i < len(hs.buckets); i++ { + out[i] = hs.buckets[i].Count() + } + return out +} +func (hs HitSet) Times() []time.Time { + out := make([]time.Time, len(hs.buckets)) + for i := 0; i < len(hs.buckets); i++ { + out[i] = hs.buckets[i].Time() + } + return out +} func (hs HitSet) YMax() int { hs.updateMinMax() diff --git a/store/sqlite3.go b/store/sqlite3.go index a889547..9525158 100644 --- a/store/sqlite3.go +++ b/store/sqlite3.go @@ -68,6 +68,10 @@ func (s *Sqlite3) SaveSite(d *sws.Site) error { func (s *Sqlite3) GetHits(d sws.Site, filter map[string]interface{}) ([]*sws.Hit, error) { hits := make([]*sws.Hit, 0) + if filter == nil { + filter = make(map[string]interface{}) + } + sql := stmts["hits"] filter["site_id"] = *d.ID processFilter(&sql, filter) diff --git a/tmpl/site.tmpl b/tmpl/site.tmpl index 923cbc3..7ca2cdf 100644 --- a/tmpl/site.tmpl +++ b/tmpl/site.tmpl @@ -131,6 +131,7 @@ </header> {{ if .Browsers }} <figure class="figure figure--graph"> + <img src="{{ piechart .Site.ID "b" .Begin .End }}" /> {{ template "barChart" .Browsers }} </figure> {{ $sum := .Browsers.YSum }} |
