github.com/Azareal/Gosora@v0.0.0-20210729070923-553e66b59003/routes/panel/dashboard.go (about)

     1  package panel
     2  
     3  import (
     4  	"database/sql"
     5  	"encoding/json"
     6  	"fmt"
     7  	"net/http"
     8  	"runtime"
     9  	"strconv"
    10  	"sync"
    11  	"sync/atomic"
    12  	"time"
    13  
    14  	c "github.com/Azareal/Gosora/common"
    15  	p "github.com/Azareal/Gosora/common/phrases"
    16  	qgen "github.com/Azareal/Gosora/query_gen"
    17  	"github.com/Azareal/gopsutil/mem"
    18  	"github.com/pkg/errors"
    19  )
    20  
    21  type dashStmts struct {
    22  	todaysPostCount         *sql.Stmt
    23  	todaysTopicCount        *sql.Stmt
    24  	todaysTopicCountByForum *sql.Stmt
    25  	todaysNewUserCount      *sql.Stmt
    26  	weeklyTopicCountByForum *sql.Stmt
    27  }
    28  
    29  // TODO: Stop hard-coding these queries
    30  func dashMySQLStmts() (stmts dashStmts, err error) {
    31  	db := qgen.Builder.GetConn()
    32  	prepStmt := func(table, ext, dur string) *sql.Stmt {
    33  		if err != nil {
    34  			return nil
    35  		}
    36  		stmt, ierr := db.Prepare("select count(*) from " + table + " where createdAt BETWEEN (utc_timestamp() - interval 1 " + dur + ") and utc_timestamp() " + ext)
    37  		err = errors.WithStack(ierr)
    38  		return stmt
    39  	}
    40  
    41  	stmts.todaysPostCount = prepStmt("replies", "", "day")
    42  	stmts.todaysTopicCount = prepStmt("topics", "", "day")
    43  	stmts.todaysNewUserCount = prepStmt("users", "", "day")
    44  	stmts.todaysTopicCountByForum = prepStmt("topics", " and parentID=?", "day")
    45  	stmts.weeklyTopicCountByForum = prepStmt("topics", " and parentID=?", "week")
    46  
    47  	return stmts, err
    48  }
    49  
    50  // TODO: Stop hard-coding these queries
    51  func dashMSSQLStmts() (stmts dashStmts, err error) {
    52  	db := qgen.Builder.GetConn()
    53  	prepStmt := func(table, ext, dur string) *sql.Stmt {
    54  		if err != nil {
    55  			return nil
    56  		}
    57  		stmt, ierr := db.Prepare("select count(*) from " + table + " where createdAt >= DATEADD(" + dur + ", -1, GETUTCDATE())" + ext)
    58  		err = errors.WithStack(ierr)
    59  		return stmt
    60  	}
    61  
    62  	stmts.todaysPostCount = prepStmt("replies", "", "DAY")
    63  	stmts.todaysTopicCount = prepStmt("topics", "", "DAY")
    64  	stmts.todaysNewUserCount = prepStmt("users", "", "DAY")
    65  	stmts.todaysTopicCountByForum = prepStmt("topics", " and parentID=?", "DAY")
    66  	stmts.weeklyTopicCountByForum = prepStmt("topics", " and parentID=?", "WEEK")
    67  
    68  	return stmts, err
    69  }
    70  
    71  type GE = c.GridElement
    72  
    73  func Dashboard(w http.ResponseWriter, r *http.Request, user *c.User) c.RouteError {
    74  	basePage, ferr := buildBasePage(w, r, user, "dashboard", "dashboard")
    75  	if ferr != nil {
    76  		return ferr
    77  	}
    78  	unknown := p.GetTmplPhrase("panel_dashboard_unknown")
    79  
    80  	// We won't calculate this on the spot anymore, as the system doesn't seem to like it if we do multiple fetches simultaneously. Should we constantly calculate this on a background thread? Perhaps, the watchdog to scale back heavy features under load? One plus side is that we'd get immediate CPU percentages here instead of waiting it to kick in with WebSockets
    81  	cpustr := unknown
    82  	var cpuColour string
    83  
    84  	lessThanSwitch := func(number, lowerBound, midBound int) string {
    85  		switch {
    86  		case number < lowerBound:
    87  			return "stat_green"
    88  		case number < midBound:
    89  			return "stat_orange"
    90  		}
    91  		return "stat_red"
    92  	}
    93  
    94  	var ramstr, ramColour string
    95  	memres, err := mem.VirtualMemory()
    96  	if err != nil {
    97  		ramstr = unknown
    98  	} else {
    99  		totalCount, totalUnit := c.ConvertByteUnit(float64(memres.Total))
   100  		usedCount := c.ConvertByteInUnit(float64(memres.Total-memres.Available), totalUnit)
   101  
   102  		// Round totals with .9s up, it's how most people see it anyway. Floats are notoriously imprecise, so do it off 0.85
   103  		var totstr string
   104  		if (totalCount - float64(int(totalCount))) > 0.85 {
   105  			usedCount += 1.0 - (totalCount - float64(int(totalCount)))
   106  			totstr = strconv.Itoa(int(totalCount) + 1)
   107  		} else {
   108  			totstr = fmt.Sprintf("%.1f", totalCount)
   109  		}
   110  		if usedCount > totalCount {
   111  			usedCount = totalCount
   112  		}
   113  		ramstr = fmt.Sprintf("%.1f", usedCount) + " / " + totstr + totalUnit
   114  
   115  		ramperc := ((memres.Total - memres.Available) * 100) / memres.Total
   116  		ramColour = lessThanSwitch(int(ramperc), 50, 75)
   117  	}
   118  
   119  	var m runtime.MemStats
   120  	runtime.ReadMemStats(&m)
   121  	memCount, memUnit := c.ConvertByteUnit(float64(m.Sys))
   122  
   123  	greaterThanSwitch := func(number, lowerBound, midBound int) string {
   124  		switch {
   125  		case number > midBound:
   126  			return "stat_green"
   127  		case number > lowerBound:
   128  			return "stat_orange"
   129  		}
   130  		return "stat_red"
   131  	}
   132  
   133  	// TODO: Add a stat store for this?
   134  	var intErr error
   135  	extractStat := func(stmt *sql.Stmt, args ...interface{}) (stat int) {
   136  		err := stmt.QueryRow(args...).Scan(&stat)
   137  		if err != nil && err != sql.ErrNoRows {
   138  			intErr = err
   139  		}
   140  		return stat
   141  	}
   142  
   143  	var stmts dashStmts
   144  	switch qgen.Builder.GetAdapter().GetName() {
   145  	case "mysql":
   146  		stmts, err = dashMySQLStmts()
   147  	case "mssql":
   148  		stmts, err = dashMSSQLStmts()
   149  	default:
   150  		return c.InternalError(errors.New("Unknown database adapter on dashboard"), w, r)
   151  	}
   152  	if err != nil {
   153  		return c.InternalError(err, w, r)
   154  	}
   155  
   156  	// TODO: Allow for more complex phrase structures than just suffixes
   157  	postCount := extractStat(stmts.todaysPostCount)
   158  	postInterval := p.GetTmplPhrase("panel_dashboard_day_suffix")
   159  	postColour := greaterThanSwitch(postCount, 5, 25)
   160  
   161  	topicCount := extractStat(stmts.todaysTopicCount)
   162  	topicInterval := p.GetTmplPhrase("panel_dashboard_day_suffix")
   163  	topicColour := greaterThanSwitch(topicCount, 0, 8)
   164  
   165  	reportCount := extractStat(stmts.weeklyTopicCountByForum, c.ReportForumID)
   166  	reportInterval := p.GetTmplPhrase("panel_dashboard_week_suffix")
   167  
   168  	newUserCount := extractStat(stmts.todaysNewUserCount)
   169  	newUserInterval := p.GetTmplPhrase("panel_dashboard_week_suffix")
   170  
   171  	// Did any of the extractStats fail?
   172  	if intErr != nil {
   173  		return c.InternalError(intErr, w, r)
   174  	}
   175  
   176  	grid1 := []GE{}
   177  	addElem1 := func(id, href, body string, order int, class, back, textColour, tooltip string) {
   178  		grid1 = append(grid1, GE{id, href, body, order, class, back, textColour, tooltip})
   179  	}
   180  	gridElements := []GE{}
   181  	addElem := func(id, href, body string, order int, class, back, textColour, tooltip string) {
   182  		gridElements = append(gridElements, GE{id, href, body, order, class, back, textColour, tooltip})
   183  	}
   184  
   185  	// TODO: Implement a check for new versions of Gosora
   186  	// TODO: Localise this
   187  	//addElem1("dash-version", "", "v" + version.String(), 0, "grid_istat stat_green", "", "", "Gosora is up-to-date :)")
   188  	addElem1("dash-version", "", "v"+c.SoftwareVersion.String(), 0, "grid_istat", "", "", "")
   189  
   190  	addElem1("dash-cpu", "", p.GetTmplPhrasef("panel_dashboard_cpu", cpustr), 1, "grid_istat "+cpuColour, "", "", p.GetTmplPhrase("panel_dashboard_cpu_desc"))
   191  	addElem1("dash-ram", "", p.GetTmplPhrasef("panel_dashboard_ram", ramstr), 2, "grid_istat "+ramColour, "", "", p.GetTmplPhrase("panel_dashboard_ram_desc"))
   192  	addElem1("dash-memused", "/panel/analytics/memory/", p.GetTmplPhrasef("panel_dashboard_memused", memCount, memUnit), 2, "grid_istat", "", "", p.GetTmplPhrase("panel_dashboard_memused_desc"))
   193  
   194  	/*dirSize := getDirSize()
   195  	if dirSize.Size != 0 {
   196  		dirFloat, unit := c.ConvertByteUnit(float64(dirSize.Size))
   197  		addElem1("dash-disk","", p.GetTmplPhrasef("panel_dashboard_disk", dirFloat, unit), 2, "grid_istat", "", "", p.GetTmplPhrase("panel_dashboard_disk_desc"))
   198  		dur := time.Since(dirSize.Time)
   199  		if dur.Seconds() > 3 {
   200  			startDirSizeTask()
   201  		}
   202  	} else {
   203  		addElem1("dash-disk","", p.GetTmplPhrase("panel_dashboard_disk_unknown"), 2, "grid_istat", "", "", p.GetTmplPhrase("panel_dashboard_disk_desc"))
   204  		startDirSizeTask()
   205  	}*/
   206  
   207  	if c.EnableWebsockets {
   208  		uonline := c.WsHub.UserCount()
   209  		gonline := c.WsHub.GuestCount()
   210  		totonline := uonline + gonline
   211  		//reqCount := 0
   212  
   213  		onlineColour := greaterThanSwitch(totonline, 3, 10)
   214  		onlineGuestsColour := greaterThanSwitch(gonline, 1, 10)
   215  		onlineUsersColour := greaterThanSwitch(uonline, 1, 5)
   216  
   217  		totonline, totunit := c.ConvertFriendlyUnit(totonline)
   218  		uonline, uunit := c.ConvertFriendlyUnit(uonline)
   219  		gonline, gunit := c.ConvertFriendlyUnit(gonline)
   220  
   221  		addElem("dash-totonline", "", p.GetTmplPhrasef("panel_dashboard_online", totonline, totunit), 3, "grid_stat "+onlineColour, "", "", p.GetTmplPhrase("panel_dashboard_online_desc"))
   222  		addElem("dash-gonline", "", p.GetTmplPhrasef("panel_dashboard_guests_online", gonline, gunit), 4, "grid_stat "+onlineGuestsColour, "", "", p.GetTmplPhrase("panel_dashboard_guests_online_desc"))
   223  		addElem("dash-uonline", "", p.GetTmplPhrasef("panel_dashboard_users_online", uonline, uunit), 5, "grid_stat "+onlineUsersColour, "", "", p.GetTmplPhrase("panel_dashboard_users_online_desc"))
   224  		//addElem("dash-reqs","", strconv.Itoa(reqCount) + " reqs / second", 7, "grid_stat grid_end_group " + topicColour, "", "", "The number of requests over the last 24 hours")
   225  	}
   226  
   227  	addElem("dash-postsperday", "", p.GetTmplPhrasef("panel_dashboard_posts", postCount, postInterval), 6, "grid_stat "+postColour, "", "", p.GetTmplPhrase("panel_dashboard_posts_desc"))
   228  	addElem("dash-topicsperday", "", p.GetTmplPhrasef("panel_dashboard_topics", topicCount, topicInterval), 7, "grid_stat "+topicColour, "", "", p.GetTmplPhrase("panel_dashboard_topics_desc"))
   229  	addElem("dash-totonlineperday", "", p.GetTmplPhrasef("panel_dashboard_online_day"), 8, "grid_stat stat_disabled", "", "", p.GetTmplPhrase("panel_dashboard_coming_soon") /*, "The people online over the last 24 hours"*/)
   230  
   231  	addElem("dash-searches", "", p.GetTmplPhrasef("panel_dashboard_searches_day"), 9, "grid_stat stat_disabled", "", "", p.GetTmplPhrase("panel_dashboard_coming_soon") /*"The number of searches over the last 7 days"*/)
   232  	addElem("dash-newusers", "", p.GetTmplPhrasef("panel_dashboard_new_users", newUserCount, newUserInterval), 10, "grid_stat", "", "", p.GetTmplPhrasef("panel_dashboard_new_users_desc"))
   233  	addElem("dash-reports", "", p.GetTmplPhrasef("panel_dashboard_reports", reportCount, reportInterval), 11, "grid_stat", "", "", p.GetTmplPhrasef("panel_dashboard_reports_desc"))
   234  
   235  	if false {
   236  		addElem("dash-minperuser", "", "?? minutes / user / week", 12, "grid_stat stat_disabled", "", "", p.GetTmplPhrase("panel_dashboard_coming_soon") /*"The average number of number of minutes spent by each active user over the last 7 days"*/)
   237  		addElem("dash-visitorsperweek", "", "?? visitors / week", 13, "grid_stat stat_disabled", "", "", p.GetTmplPhrase("panel_dashboard_coming_soon") /*"The number of unique visitors we've had over the last 7 days"*/)
   238  		addElem("dash-postsperuser", "", "?? posts / user / week", 14, "grid_stat stat_disabled", "", "", p.GetTmplPhrase("panel_dashboard_coming_soon") /*"The average number of posts made by each active user over the past week"*/)
   239  	}
   240  
   241  	return renderTemplate("panel", w, r, basePage.Header, c.Panel{basePage, "panel_dashboard_right", "", "panel_dashboard", c.DashGrids{grid1, gridElements}})
   242  }
   243  
   244  type dirSize struct {
   245  	Size int
   246  	Time time.Time
   247  }
   248  
   249  func init() {
   250  	cachedDirSize.Store(dirSize{0, time.Now()})
   251  }
   252  
   253  var cachedDirSize atomic.Value
   254  var dstMu sync.Mutex
   255  var dstMuGuess = 0
   256  
   257  func startDirSizeTask() {
   258  	if dstMuGuess == 1 {
   259  		return
   260  	}
   261  	dstMu.Lock()
   262  	dstMuGuess = 1
   263  	go func() {
   264  		defer func() {
   265  			dstMuGuess = 0
   266  			dstMu.Unlock()
   267  		}()
   268  		defer c.EatPanics()
   269  		dDirSize, e := c.DirSize(".")
   270  		if e != nil {
   271  			c.LogWarning(e)
   272  		}
   273  		cachedDirSize.Store(dirSize{dDirSize, time.Now()})
   274  	}()
   275  }
   276  
   277  func getDirSize() dirSize {
   278  	return cachedDirSize.Load().(dirSize)
   279  }
   280  
   281  type StatsDiskJson struct {
   282  	Total string `json:"total"`
   283  }
   284  
   285  func StatsDisk(w http.ResponseWriter, r *http.Request, user *c.User) c.RouteError {
   286  	dirSize := getDirSize()
   287  	dirFloat, unit := c.ConvertByteUnit(float64(dirSize.Size))
   288  	u := p.GetTmplPhrasef("unit", dirFloat, unit)
   289  	oBytes, err := json.Marshal(StatsDiskJson{u})
   290  	if err != nil {
   291  		return c.InternalErrorJS(err, w, r)
   292  	}
   293  	w.Write(oBytes)
   294  	return nil
   295  }