Browse Source

Add `GET /api/about` that returns useful system info. Closes #1354.

Kailash Nadh 2 years ago
parent
commit
c581fe2f3a
7 changed files with 105 additions and 0 deletions
  1. 1 0
      cmd/handlers.go
  2. 40 0
      cmd/init.go
  3. 4 0
      cmd/main.go
  4. 45 0
      cmd/settings.go
  5. 10 0
      cmd/utils.go
  6. 1 0
      models/queries.go
  7. 4 0
      queries.sql

+ 1 - 0
cmd/handlers.go

@@ -86,6 +86,7 @@ func initHTTPHandlers(e *echo.Echo, app *App) {
 	g.POST("/api/settings/smtp/test", handleTestSMTPSettings)
 	g.POST("/api/admin/reload", handleReloadApp)
 	g.GET("/api/logs", handleGetLogs)
+	g.GET("/api/about", handleGetAboutInfo)
 
 	g.GET("/api/subscribers/:id", handleGetSubscriber)
 	g.GET("/api/subscribers/:id/export", handleExportSubscriberData)

+ 40 - 0
cmd/init.go

@@ -8,6 +8,7 @@ import (
 	"os"
 	"path"
 	"path/filepath"
+	"runtime"
 	"strings"
 	"syscall"
 	"time"
@@ -697,6 +698,45 @@ func initBounceManager(app *App) *bounce.Manager {
 	return b
 }
 
+func initAbout(q *models.Queries, db *sqlx.DB) about {
+	var (
+		mem     runtime.MemStats
+		utsname syscall.Utsname
+	)
+
+	// Memory / alloc stats.
+	runtime.ReadMemStats(&mem)
+
+	// OS info.
+	if err := syscall.Uname(&utsname); err != nil {
+		lo.Printf("WARNING: error getting system info: %v", err)
+	}
+
+	// DB dbv.
+	info := types.JSONText(`{}`)
+	if err := db.QueryRow(q.GetDBInfo).Scan(&info); err != nil {
+		lo.Printf("WARNING: error getting database version: %v", err)
+	}
+
+	return about{
+		Version:   versionString,
+		Build:     buildString,
+		GoArch:    runtime.GOARCH,
+		GoVersion: runtime.Version(),
+		Database:  info,
+		System: aboutSystem{
+			NumCPU: runtime.NumCPU(),
+		},
+		Host: aboutHost{
+			OS:        int8ToStr(utsname.Sysname[:]),
+			OSRelease: int8ToStr(utsname.Release[:]),
+			Machine:   int8ToStr(utsname.Machine[:]),
+			Hostname:  int8ToStr(utsname.Nodename[:]),
+		},
+	}
+
+}
+
 // initHTTPServer sets up and runs the app's main HTTP server and blocks forever.
 func initHTTPServer(app *App) *echo.Echo {
 	// Initialize the HTTP server.

+ 4 - 0
cmd/main.go

@@ -51,6 +51,7 @@ type App struct {
 	captcha    *captcha.Captcha
 	events     *events.Events
 	notifTpls  *notifTpls
+	about      about
 	log        *log.Logger
 	bufLog     *buflog.BufLog
 
@@ -229,6 +230,9 @@ func main() {
 		app.manager.AddMessenger(m)
 	}
 
+	// Load system information.
+	app.about = initAbout(queries, db)
+
 	// Start the campaign workers. The campaign batches (fetch from DB, push out
 	// messages) get processed at the specified interval.
 	go app.manager.Run()

+ 45 - 0
cmd/settings.go

@@ -2,14 +2,17 @@ package main
 
 import (
 	"bytes"
+	"fmt"
 	"io/ioutil"
 	"net/http"
 	"regexp"
+	"runtime"
 	"strings"
 	"syscall"
 	"time"
 
 	"github.com/gofrs/uuid"
+	"github.com/jmoiron/sqlx/types"
 	"github.com/knadh/koanf/parsers/json"
 	"github.com/knadh/koanf/providers/rawbytes"
 	"github.com/knadh/koanf/v2"
@@ -18,6 +21,27 @@ import (
 	"github.com/labstack/echo/v4"
 )
 
+type aboutHost struct {
+	OS        string `json:"os"`
+	OSRelease string `json:"os_release"`
+	Machine   string `json:"arch"`
+	Hostname  string `json:"hostname"`
+}
+type aboutSystem struct {
+	NumCPU  int    `json:"num_cpu"`
+	AllocMB uint64 `json:"memory_alloc_mb"`
+	OSMB    uint64 `json:"memory_from_os_mb"`
+}
+type about struct {
+	Version   string         `json:"version"`
+	Build     string         `json:"build"`
+	GoVersion string         `json:"go_version"`
+	GoArch    string         `json:"go_arch"`
+	Database  types.JSONText `json:"database"`
+	System    aboutSystem    `json:"system"`
+	Host      aboutHost      `json:"host"`
+}
+
 var (
 	reAlphaNum = regexp.MustCompile(`[^a-z0-9\-]`)
 )
@@ -266,3 +290,24 @@ func handleTestSMTPSettings(c echo.Context) error {
 
 	return c.JSON(http.StatusOK, okResp{app.bufLog.Lines()})
 }
+
+func handleGetAboutInfo(c echo.Context) error {
+	app := c.Get("app").(*App)
+
+	var (
+		mem     runtime.MemStats
+		utsname syscall.Utsname
+	)
+
+	runtime.ReadMemStats(&mem)
+
+	if err := syscall.Uname(&utsname); err != nil {
+		return echo.NewHTTPError(http.StatusInternalServerError, fmt.Errorf("error getting system info: %v", err))
+	}
+
+	out := app.about
+	out.System.AllocMB = mem.Alloc / 1024 / 1024
+	out.System.OSMB = mem.Sys / 1024 / 1024
+
+	return c.JSON(http.StatusOK, out)
+}

+ 10 - 0
cmd/utils.go

@@ -1,6 +1,7 @@
 package main
 
 import (
+	"bytes"
 	"crypto/rand"
 	"fmt"
 	"path/filepath"
@@ -99,3 +100,12 @@ func strSliceContains(str string, sl []string) bool {
 
 	return false
 }
+
+func int8ToStr(bs []int8) string {
+	b := make([]byte, len(bs))
+	for i, v := range bs {
+		b[i] = byte(v)
+	}
+
+	return string(bytes.Trim(b, "\x00"))
+}

+ 1 - 0
models/queries.go

@@ -106,6 +106,7 @@ type Queries struct {
 	QueryBounces              string     `query:"query-bounces"`
 	DeleteBounces             *sqlx.Stmt `query:"delete-bounces"`
 	DeleteBouncesBySubscriber *sqlx.Stmt `query:"delete-bounces-by-subscriber"`
+	GetDBInfo                 string     `query:"get-db-info"`
 }
 
 // CompileSubscriberQueryTpl takes an arbitrary WHERE expressions

+ 4 - 0
queries.sql

@@ -1067,3 +1067,7 @@ WITH sub AS (
 )
 DELETE FROM bounces WHERE subscriber_id = (SELECT id FROM sub);
 
+
+-- name: get-db-info
+SELECT JSON_BUILD_OBJECT('version', (SELECT VERSION()),
+                        'size_mb', (SELECT ROUND(pg_database_size('listmonk')/(1024^2)))) AS info;