From 7d87aad55a3718c7365663912095cafa7b8aaa70 Mon Sep 17 00:00:00 2001 From: Matt Baer Date: Sun, 18 Nov 2018 20:18:22 -0500 Subject: [PATCH] Add basic admin dashboard with app stats Start of T538 --- account.go | 18 +++--- admin.go | 103 +++++++++++++++++++++++++++++++++ handle.go | 38 ++++++++++++ routes.go | 2 + templates/user/admin.tmpl | 108 +++++++++++++++++++++++++++++++++++ templates/user/settings.tmpl | 2 +- users.go | 5 ++ 7 files changed, 267 insertions(+), 9 deletions(-) create mode 100644 templates/user/admin.tmpl diff --git a/account.go b/account.go index 1edba1a..5612389 100644 --- a/account.go +++ b/account.go @@ -34,17 +34,19 @@ type ( PageTitle string Separator template.HTML + IsAdmin bool } ) -func NewUserPage(app *app, r *http.Request, username, title string, flashes []string) *UserPage { +func NewUserPage(app *app, r *http.Request, u *User, title string, flashes []string) *UserPage { up := &UserPage{ StaticPage: pageForReq(app, r), PageTitle: title, } - up.Username = username + up.Username = u.Username up.Flashes = flashes up.Path = r.URL.Path + up.IsAdmin = u.IsAdmin() return up } @@ -538,7 +540,7 @@ func getVerboseAuthUser(app *app, token string, u *User, verbose bool) *AuthUser func viewExportOptions(app *app, u *User, w http.ResponseWriter, r *http.Request) error { // Fetch extra user data - p := NewUserPage(app, r, u.Username, "Export", nil) + p := NewUserPage(app, r, u, "Export", nil) showUserPage(w, "export", p) return nil @@ -722,7 +724,7 @@ func viewArticles(app *app, u *User, w http.ResponseWriter, r *http.Request) err AnonymousPosts *[]PublicPost Collections *[]Collection }{ - UserPage: NewUserPage(app, r, u.Username, u.Username+"'s Posts", f), + UserPage: NewUserPage(app, r, u, u.Username+"'s Posts", f), AnonymousPosts: p, Collections: c, } @@ -754,7 +756,7 @@ func viewCollections(app *app, u *User, w http.ResponseWriter, r *http.Request) NewBlogsDisabled bool }{ - UserPage: NewUserPage(app, r, u.Username, u.Username+"'s Blogs", f), + UserPage: NewUserPage(app, r, u, u.Username+"'s Blogs", f), Collections: c, UsedCollections: int(uc), NewBlogsDisabled: !app.cfg.App.CanCreateBlogs(uc), @@ -780,7 +782,7 @@ func viewEditCollection(app *app, u *User, w http.ResponseWriter, r *http.Reques *UserPage *Collection }{ - UserPage: NewUserPage(app, r, u.Username, "Edit "+c.DisplayTitle(), flashes), + UserPage: NewUserPage(app, r, u, "Edit "+c.DisplayTitle(), flashes), Collection: c, } @@ -952,7 +954,7 @@ func viewStats(app *app, u *User, w http.ResponseWriter, r *http.Request) error TopPosts *[]PublicPost APFollowers int }{ - UserPage: NewUserPage(app, r, u.Username, titleStats+"Stats", flashes), + UserPage: NewUserPage(app, r, u, titleStats+"Stats", flashes), VisitsBlog: alias, Collection: c, TopPosts: topPosts, @@ -990,7 +992,7 @@ func viewSettings(app *app, u *User, w http.ResponseWriter, r *http.Request) err HasPass bool IsLogOut bool }{ - UserPage: NewUserPage(app, r, u.Username, "Account Settings", flashes), + UserPage: NewUserPage(app, r, u, "Account Settings", flashes), Email: fullUser.EmailClear(app.keys), HasPass: passIsSet, IsLogOut: r.FormValue("logout") == "1", diff --git a/admin.go b/admin.go index 29cff31..6d3cffd 100644 --- a/admin.go +++ b/admin.go @@ -2,11 +2,114 @@ package writefreely import ( "fmt" + "github.com/gogits/gogs/pkg/tool" "github.com/writeas/impart" "github.com/writeas/web-core/auth" "net/http" + "runtime" + "time" ) +var ( + appStartTime = time.Now() + sysStatus systemStatus +) + +type systemStatus struct { + Uptime string + NumGoroutine int + + // General statistics. + MemAllocated string // bytes allocated and still in use + MemTotal string // bytes allocated (even if freed) + MemSys string // bytes obtained from system (sum of XxxSys below) + Lookups uint64 // number of pointer lookups + MemMallocs uint64 // number of mallocs + MemFrees uint64 // number of frees + + // Main allocation heap statistics. + HeapAlloc string // bytes allocated and still in use + HeapSys string // bytes obtained from system + HeapIdle string // bytes in idle spans + HeapInuse string // bytes in non-idle span + HeapReleased string // bytes released to the OS + HeapObjects uint64 // total number of allocated objects + + // Low-level fixed-size structure allocator statistics. + // Inuse is bytes used now. + // Sys is bytes obtained from system. + StackInuse string // bootstrap stacks + StackSys string + MSpanInuse string // mspan structures + MSpanSys string + MCacheInuse string // mcache structures + MCacheSys string + BuckHashSys string // profiling bucket hash table + GCSys string // GC metadata + OtherSys string // other system allocations + + // Garbage collector statistics. + NextGC string // next run in HeapAlloc time (bytes) + LastGC string // last run in absolute time (ns) + PauseTotalNs string + PauseNs string // circular buffer of recent GC pause times, most recent at [(NumGC+255)%256] + NumGC uint32 +} + +func handleViewAdminDash(app *app, u *User, w http.ResponseWriter, r *http.Request) error { + updateAppStats() + p := struct { + *UserPage + Message string + SysStatus systemStatus + }{ + NewUserPage(app, r, u, "Admin", nil), + r.FormValue("m"), + sysStatus, + } + + showUserPage(w, "admin", p) + return nil +} + +func updateAppStats() { + sysStatus.Uptime = tool.TimeSincePro(appStartTime) + + m := new(runtime.MemStats) + runtime.ReadMemStats(m) + sysStatus.NumGoroutine = runtime.NumGoroutine() + + sysStatus.MemAllocated = tool.FileSize(int64(m.Alloc)) + sysStatus.MemTotal = tool.FileSize(int64(m.TotalAlloc)) + sysStatus.MemSys = tool.FileSize(int64(m.Sys)) + sysStatus.Lookups = m.Lookups + sysStatus.MemMallocs = m.Mallocs + sysStatus.MemFrees = m.Frees + + sysStatus.HeapAlloc = tool.FileSize(int64(m.HeapAlloc)) + sysStatus.HeapSys = tool.FileSize(int64(m.HeapSys)) + sysStatus.HeapIdle = tool.FileSize(int64(m.HeapIdle)) + sysStatus.HeapInuse = tool.FileSize(int64(m.HeapInuse)) + sysStatus.HeapReleased = tool.FileSize(int64(m.HeapReleased)) + sysStatus.HeapObjects = m.HeapObjects + + sysStatus.StackInuse = tool.FileSize(int64(m.StackInuse)) + sysStatus.StackSys = tool.FileSize(int64(m.StackSys)) + sysStatus.MSpanInuse = tool.FileSize(int64(m.MSpanInuse)) + sysStatus.MSpanSys = tool.FileSize(int64(m.MSpanSys)) + sysStatus.MCacheInuse = tool.FileSize(int64(m.MCacheInuse)) + sysStatus.MCacheSys = tool.FileSize(int64(m.MCacheSys)) + sysStatus.BuckHashSys = tool.FileSize(int64(m.BuckHashSys)) + sysStatus.GCSys = tool.FileSize(int64(m.GCSys)) + sysStatus.OtherSys = tool.FileSize(int64(m.OtherSys)) + + sysStatus.NextGC = tool.FileSize(int64(m.NextGC)) + sysStatus.LastGC = fmt.Sprintf("%.1fs", float64(time.Now().UnixNano()-int64(m.LastGC))/1000/1000/1000) + sysStatus.PauseTotalNs = fmt.Sprintf("%.1fs", float64(m.PauseTotalNs)/1000/1000/1000) + sysStatus.PauseNs = fmt.Sprintf("%.3fs", float64(m.PauseNs[(m.NumGC+255)%256])/1000/1000/1000) + sysStatus.NumGC = m.NumGC +} + func adminResetPassword(app *app, u *User, newPass string) error { hashedPass, err := auth.HashPass([]byte(newPass)) if err != nil { diff --git a/handle.go b/handle.go index 6fde474..62ff436 100644 --- a/handle.go +++ b/handle.go @@ -109,6 +109,44 @@ func (h *Handler) User(f userHandlerFunc) http.HandlerFunc { } } +// Admin handles requests on /admin routes +func (h *Handler) Admin(f userHandlerFunc) http.HandlerFunc { + return func(w http.ResponseWriter, r *http.Request) { + h.handleHTTPError(w, r, func() error { + var status int + start := time.Now() + + defer func() { + if e := recover(); e != nil { + log.Error("%s: %s", e, debug.Stack()) + h.errors.InternalServerError.ExecuteTemplate(w, "base", pageForReq(h.app, r)) + status = http.StatusInternalServerError + } + + log.Info(fmt.Sprintf("\"%s %s\" %d %s \"%s\"", r.Method, r.RequestURI, status, time.Since(start), r.UserAgent())) + }() + + u := getUserSession(h.app, r) + if u == nil || !u.IsAdmin() { + err := impart.HTTPError{http.StatusNotFound, ""} + status = err.Status + return err + } + + err := f(h.app, u, w, r) + if err == nil { + status = http.StatusOK + } else if err, ok := err.(impart.HTTPError); ok { + status = err.Status + } else { + status = http.StatusInternalServerError + } + + return err + }()) + } +} + // UserAPI handles requests made in the API by the authenticated user. // This provides user-friendly HTML pages and actions that work in the browser. func (h *Handler) UserAPI(f userHandlerFunc) http.HandlerFunc { diff --git a/routes.go b/routes.go index 21cbda4..28a1d3f 100644 --- a/routes.go +++ b/routes.go @@ -115,6 +115,8 @@ func initRoutes(handler *Handler, r *mux.Router, cfg *config.Config, db *datasto } write.HandleFunc("/auth/login", handler.Web(webLogin, UserLevelNoneRequired)).Methods("POST") + write.HandleFunc("/admin", handler.Admin(handleViewAdminDash)).Methods("GET") + // Handle special pages first write.HandleFunc("/login", handler.Web(viewLogin, UserLevelNoneRequired)) diff --git a/templates/user/admin.tmpl b/templates/user/admin.tmpl new file mode 100644 index 0000000..cf6ac8f --- /dev/null +++ b/templates/user/admin.tmpl @@ -0,0 +1,108 @@ +{{define "admin"}} +{{template "header" .}} + + + +
+

Admin Dashboard

+ + {{if .Message}}

{{.Message}}

{{end}} + + + +
+ +

application monitor

+
+
+
Server Uptime
+
{{.SysStatus.Uptime}}
+
Current Goroutines
+
{{.SysStatus.NumGoroutine}}
+
+
Current memory usage
+
{{.SysStatus.MemAllocated}}
+
Total mem allocated
+
{{.SysStatus.MemTotal}}
+
Memory obtained
+
{{.SysStatus.MemSys}}
+
Pointer lookup times
+
{{.SysStatus.Lookups}}
+
Memory allocate times
+
{{.SysStatus.MemMallocs}}
+
Memory free times
+
{{.SysStatus.MemFrees}}
+
+
Current heap usage
+
{{.SysStatus.HeapAlloc}}
+
Heap memory obtained
+
{{.SysStatus.HeapSys}}
+
Heap memory idle
+
{{.SysStatus.HeapIdle}}
+
Heap memory in use
+
{{.SysStatus.HeapInuse}}
+
Heap memory released
+
{{.SysStatus.HeapReleased}}
+
Heap objects
+
{{.SysStatus.HeapObjects}}
+
+
Bootstrap stack usage
+
{{.SysStatus.StackInuse}}
+
Stack memory obtained
+
{{.SysStatus.StackSys}}
+
MSpan structures in use
+
{{.SysStatus.MSpanInuse}}
+
MSpan structures obtained
+
{{.SysStatus.HeapSys}}
+
MCache structures in use
+
{{.SysStatus.MCacheInuse}}
+
MCache structures obtained
+
{{.SysStatus.MCacheSys}}
+
Profiling bucket hash table obtained
+
{{.SysStatus.BuckHashSys}}
+
GC metadata obtained
+
{{.SysStatus.GCSys}}
+
Other system allocation obtained
+
{{.SysStatus.OtherSys}}
+
+
Next GC recycle
+
{{.SysStatus.NextGC}}
+
Since last GC
+
{{.SysStatus.LastGC}}
+
Total GC pause
+
{{.SysStatus.PauseTotalNs}}
+
Last GC pause
+
{{.SysStatus.PauseNs}}
+
GC times
+
{{.SysStatus.NumGC}}
+
+
+
+{{template "footer" .}} +{{template "body-end" .}} +{{end}} diff --git a/templates/user/settings.tmpl b/templates/user/settings.tmpl index 204aae3..fd204a5 100644 --- a/templates/user/settings.tmpl +++ b/templates/user/settings.tmpl @@ -7,7 +7,7 @@ h3 { font-weight: normal; } .section > *:not(input) { font-size: 0.86em; }
-

{{if .IsLogOut}}Before you go...{{else}}Account Settings{{end}}

+

{{if .IsLogOut}}Before you go...{{else}}Account Settings {{if .IsAdmin}}admin settings{{end}}{{end}}

{{if .Flashes}}{{end}} diff --git a/users.go b/users.go index ec7af21..b645c6a 100644 --- a/users.go +++ b/users.go @@ -92,3 +92,8 @@ func (u User) Cookie() *User { return &u } + +func (u *User) IsAdmin() bool { + // TODO: get this from database + return u.ID == 1 +}