diff --git a/cmd/handlers.go b/cmd/handlers.go index 828d79f..35008e0 100644 --- a/cmd/handlers.go +++ b/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) diff --git a/cmd/init.go b/cmd/init.go index f88542f..d47c505 100644 --- a/cmd/init.go +++ b/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. diff --git a/cmd/main.go b/cmd/main.go index 67b4652..d45a7b7 100644 --- a/cmd/main.go +++ b/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() diff --git a/cmd/settings.go b/cmd/settings.go index 0159dc1..03bacda 100644 --- a/cmd/settings.go +++ b/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) +} diff --git a/cmd/utils.go b/cmd/utils.go index 1888e6c..ef2b6f2 100644 --- a/cmd/utils.go +++ b/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")) +} diff --git a/models/queries.go b/models/queries.go index 21fabd2..c31c6e8 100644 --- a/models/queries.go +++ b/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 diff --git a/queries.sql b/queries.sql index 7b3f11d..d0ffa9a 100644 --- a/queries.sql +++ b/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;