From 6f21095f6d71e0d5e2b75e754f9b28e15a552d36 Mon Sep 17 00:00:00 2001 From: Felix Hanley Date: Thu, 5 Mar 2020 22:45:33 +1100 Subject: Add some basic time ranges --- charts.go | 3 ++- cmd/server/charts.go | 2 +- cmd/server/handlers.go | 23 ++++++++++++----------- cmd/server/helpers.go | 38 ++++++++++++++++++++++++++++++++------ cmd/server/main.go | 28 ++++++++++++++++++++-------- cmd/server/routes.go | 2 +- cmd/server/sites.go | 10 ++++++++++ hit_set.go | 12 +++++++++--- page_set.go | 2 +- templates/charts.tmpl | 2 +- templates/site.tmpl | 33 ++++++++++++++++++++++++++++++--- templates/timerange.tmpl | 38 ++++++++++++++++++++++++++++++++++++++ 12 files changed, 157 insertions(+), 36 deletions(-) create mode 100644 templates/timerange.tmpl diff --git a/charts.go b/charts.go index 937edc4..fe63dbe 100644 --- a/charts.go +++ b/charts.go @@ -68,8 +68,9 @@ func SparklineSVG(w io.Writer, data *HitSet, d time.Duration) error { StrokeColor: drawing.Color{R: 0, G: 0, B: 255, A: 100}, }, } + //data, _ = NewHitSet(FromHits(data.Hits()), Duration(d)) + //data.SortByDate() - data.SortByDate() var xVals []time.Time var yVals []float64 tmp := data.XSeries() diff --git a/cmd/server/charts.go b/cmd/server/charts.go index 59fd6d2..875b528 100644 --- a/cmd/server/charts.go +++ b/cmd/server/charts.go @@ -56,7 +56,7 @@ func sparklineHandler(db sws.HitStore) http.HandlerFunc { w.Header().Set("Content-Type", "image/svg+xml") w.Header().Set("Cache-Control", "public") - sws.SparklineSVG(w, data, time.Minute) + sws.SparklineSVG(w, data, time.Hour) } } diff --git a/cmd/server/handlers.go b/cmd/server/handlers.go index 18ee0fa..d13109d 100644 --- a/cmd/server/handlers.go +++ b/cmd/server/handlers.go @@ -10,17 +10,18 @@ import ( ) type templateData struct { - Payload string - Endpoint string - User *sws.User - Flash template.HTML - Begin *time.Time - End *time.Time - Site *sws.Site - Sites []*sws.Site - PageSet sws.PageSet - Browsers sws.BrowserSet - Hits *sws.HitSet + Payload string + Endpoint string + User *sws.User + Flash template.HTML + Begin time.Time + End time.Time + Site *sws.Site + Sites []*sws.Site + PageSet sws.PageSet + Browsers sws.BrowserSet + ReferrerSet sws.ReferrerSet + Hits *sws.HitSet } func newTemplateData(r *http.Request) *templateData { diff --git a/cmd/server/helpers.go b/cmd/server/helpers.go index 29f15ab..01552b2 100644 --- a/cmd/server/helpers.go +++ b/cmd/server/helpers.go @@ -11,8 +11,9 @@ import ( var funcMap = template.FuncMap{ "sparkline": func(id int) string { // This will enable "caching" for an hour - now := time.Now().Truncate(time.Hour) - then := now.Add(-168 * time.Hour) + now := time.Now() //.Truncate(time.Hour) + //then := now.Add(-720 * 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()) @@ -22,15 +23,40 @@ var funcMap = template.FuncMap{ // TODO error return t.In(tz) }, - "timeShort": func(t time.Time) string { + /* + "seq": func(start, stop, step int) []int { + count := (stop - start) / step + out := make([]int, count) + c := start + for i := 0; i < count; i++ { + out[i] = c + c += step + } + return out + }, + "div": func(a, b int) int { + return a / b + }, + */ + "datetimeShort": func(t time.Time) string { return t.Format("2006-01-02 15:04") }, - "timeLong": func(t time.Time) string { + "datetimeLong": func(t time.Time) string { return t.Format(time.RFC3339) }, - "timeHour": func(t time.Time) string { + "datetimeHour": func(t time.Time) string { return t.Format("15:04 Jan 2") }, + "dateRFC": func(t time.Time) string { + return t.Format("2006-01-02") + }, + "timeRFC": func(t time.Time) string { + return t.Format("15:04") + }, + "datetimeRelative": func(d string) int64 { + dur, _ := time.ParseDuration(d) + return time.Now().Add(dur).Unix() + }, "percent": func(a, b int) float64 { return (float64(a) / float64(b)) * 100 }, @@ -46,7 +72,7 @@ func httpError(w http.ResponseWriter, code int, msg string) { func extractTimeRange(r *http.Request) (*time.Time, *time.Time) { begin := timePtr(time.Now().Truncate(time.Hour).Add(-168 * time.Hour)) - end := timePtr(time.Now()) + end := timePtr(time.Now().Truncate(time.Hour)) 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/main.go b/cmd/server/main.go index 4e98b3b..900ef25 100644 --- a/cmd/server/main.go +++ b/cmd/server/main.go @@ -3,6 +3,7 @@ package main import ( "flag" "fmt" + "io" "net/http" "os" "strings" @@ -26,6 +27,7 @@ var ( addr *string dsn *string domain *string + logFile *string noMigrate *bool ) @@ -34,6 +36,7 @@ func init() { addr = stringFlag("listen", "l", "localhost:5000", "LISTEN", "listen address") dsn = stringFlag("dsn", "", "file:sws.db?cache=shared", "DSN", "database password") domain = stringFlag("domain", "", "stats.userspace.com.au", "DOMAIN", "stats domain") + logFile = stringFlag("log", "", "", "LOGFILE", "log to file") noMigrate = boolFlag("no-migrate", "m", false, "NOMIGRATE", "disable migrations") // Default to no log @@ -47,18 +50,27 @@ type Renderer interface { } func main() { + var err error flag.Parse() + var output io.Writer = os.Stdout + if logFile != nil && *logFile != "" { + if output, err = os.Create(*logFile); err != nil { + fmt.Fprintf(os.Stderr, "failed to open log file: %s", err) + os.Exit(1) + } + } + if *verbose { log = func(v ...interface{}) { - fmt.Fprintf(os.Stdout, "[%s] ", time.Now().Format(time.RFC3339)) - fmt.Fprintln(os.Stdout, v...) + fmt.Fprintf(output, "[%s] ", time.Now().Format(time.RFC3339)) + fmt.Fprintln(output, v...) } } if d := os.Getenv("DEBUG"); d != "" { debug = func(v ...interface{}) { - fmt.Fprintf(os.Stdout, "[%s] ", time.Now().Format(time.RFC3339)) - fmt.Fprintln(os.Stdout, v...) + fmt.Fprintf(output, "[%s] ", time.Now().Format(time.RFC3339)) + fmt.Fprintln(output, v...) } } log("version", Version) @@ -71,7 +83,7 @@ func main() { if noMigrate == nil || !*noMigrate { v, err := migrateDatabase(driver, *dsn) if err != nil { - fmt.Fprintf(os.Stderr, "failed to migrate: %s", err) + log("failed to migrate:", err) os.Exit(2) } log("database at version", v) @@ -79,12 +91,12 @@ func main() { db, err := sqlx.Open(driver, *dsn) if err != nil { - log(err) + log("failed to open database:", err) os.Exit(1) } defer db.Close() if err := db.Ping(); err != nil { - log(err) + log("failed to connect to database:", err) os.Exit(1) } var st sws.Store @@ -96,7 +108,7 @@ func main() { r, err := createRouter(st) if err != nil { - log(err) + log("failed to create router:", err) os.Exit(1) } diff --git a/cmd/server/routes.go b/cmd/server/routes.go index 7d7ab32..c8d0264 100644 --- a/cmd/server/routes.go +++ b/cmd/server/routes.go @@ -24,7 +24,7 @@ func init() { func createRouter(db sws.Store) (chi.Router, error) { tmplsCommon := []string{"flash.tmpl", "navbar.tmpl"} - tmplsAuthed := append(tmplsCommon, []string{"layouts/base.tmpl", "charts.tmpl"}...) + tmplsAuthed := append(tmplsCommon, []string{"layouts/base.tmpl", "charts.tmpl", "timerange.tmpl"}...) tmplsPublic := append(tmplsCommon, "layouts/public.tmpl") tmpls, err := LoadHTMLTemplateMap(map[string][]string{ diff --git a/cmd/server/sites.go b/cmd/server/sites.go index 46bcdf3..1b09e6e 100644 --- a/cmd/server/sites.go +++ b/cmd/server/sites.go @@ -58,6 +58,10 @@ func handleSite(db sws.SiteStore, rndr Renderer) http.HandlerFunc { httpError(w, 406, "invalid time range") return } + payload.Begin = *begin + payload.End = *end + debug("begin", *begin) + debug("end", *end) hits, err := db.GetHits(*site, map[string]interface{}{ "begin": *begin, @@ -90,6 +94,12 @@ func handleSite(db sws.SiteStore, rndr Renderer) http.HandlerFunc { } browserSet := sws.NewBrowserSet(hitSet) payload.Browsers = browserSet + + refSet := sws.NewReferrerSet(hitSet) + if refSet != nil { + refSet.SortByHits() + payload.ReferrerSet = refSet + } } if err := rndr.Render(w, "site", payload); err != nil { diff --git a/hit_set.go b/hit_set.go index f34b585..cd9210f 100644 --- a/hit_set.go +++ b/hit_set.go @@ -30,10 +30,16 @@ func TimeZone(s string) HitSetOption { } } -func Duration(s string) HitSetOption { +func Duration(d time.Duration) HitSetOption { return func(hs *HitSet) error { - var err error - hs.duration, err = time.ParseDuration(s) + hs.duration = d + return nil + } +} +func DurationString(s string) HitSetOption { + return func(hs *HitSet) error { + d, err := time.ParseDuration(s) + Duration(d) return err } } diff --git a/page_set.go b/page_set.go index 9077731..c9a77ff 100644 --- a/page_set.go +++ b/page_set.go @@ -60,7 +60,7 @@ func (ps *PageSet) SortByHits() { }) } -func (ps PageSet) Page(s string) *Page { +func (ps PageSet) GetPage(s string) *Page { for _, p := range ps { if p.Path == s { return p diff --git a/templates/charts.tmpl b/templates/charts.tmpl index e6979c6..b02d3b7 100644 --- a/templates/charts.tmpl +++ b/templates/charts.tmpl @@ -37,7 +37,7 @@