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

This commit is contained in:
Kailash Nadh 2023-06-24 13:07:13 +05:30
parent 5b404615fc
commit c581fe2f3a
7 changed files with 105 additions and 0 deletions

View file

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

View file

@ -8,6 +8,7 @@ import (
"os" "os"
"path" "path"
"path/filepath" "path/filepath"
"runtime"
"strings" "strings"
"syscall" "syscall"
"time" "time"
@ -697,6 +698,45 @@ func initBounceManager(app *App) *bounce.Manager {
return b 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. // initHTTPServer sets up and runs the app's main HTTP server and blocks forever.
func initHTTPServer(app *App) *echo.Echo { func initHTTPServer(app *App) *echo.Echo {
// Initialize the HTTP server. // Initialize the HTTP server.

View file

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

View file

@ -2,14 +2,17 @@ package main
import ( import (
"bytes" "bytes"
"fmt"
"io/ioutil" "io/ioutil"
"net/http" "net/http"
"regexp" "regexp"
"runtime"
"strings" "strings"
"syscall" "syscall"
"time" "time"
"github.com/gofrs/uuid" "github.com/gofrs/uuid"
"github.com/jmoiron/sqlx/types"
"github.com/knadh/koanf/parsers/json" "github.com/knadh/koanf/parsers/json"
"github.com/knadh/koanf/providers/rawbytes" "github.com/knadh/koanf/providers/rawbytes"
"github.com/knadh/koanf/v2" "github.com/knadh/koanf/v2"
@ -18,6 +21,27 @@ import (
"github.com/labstack/echo/v4" "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 ( var (
reAlphaNum = regexp.MustCompile(`[^a-z0-9\-]`) 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()}) 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)
}

View file

@ -1,6 +1,7 @@
package main package main
import ( import (
"bytes"
"crypto/rand" "crypto/rand"
"fmt" "fmt"
"path/filepath" "path/filepath"
@ -99,3 +100,12 @@ func strSliceContains(str string, sl []string) bool {
return false 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"))
}

View file

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

View file

@ -1067,3 +1067,7 @@ WITH sub AS (
) )
DELETE FROM bounces WHERE subscriber_id = (SELECT id FROM sub); 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;