600 lines
20 KiB
Go
600 lines
20 KiB
Go
package handlers
|
|
|
|
import (
|
|
"fmt"
|
|
"io/fs"
|
|
"os"
|
|
"path/filepath"
|
|
"runtime"
|
|
"runtime/pprof"
|
|
"seanime/internal/constants"
|
|
"seanime/internal/core"
|
|
"seanime/internal/database/models"
|
|
"seanime/internal/user"
|
|
"seanime/internal/util"
|
|
"seanime/internal/util/result"
|
|
"slices"
|
|
"strconv"
|
|
"strings"
|
|
"time"
|
|
|
|
"github.com/labstack/echo/v4"
|
|
)
|
|
|
|
// Status is a struct containing the user data, settings, and OS.
|
|
// It is used by the client in various places to access necessary information.
|
|
type Status struct {
|
|
OS string `json:"os"`
|
|
ClientDevice string `json:"clientDevice"`
|
|
ClientPlatform string `json:"clientPlatform"`
|
|
ClientUserAgent string `json:"clientUserAgent"`
|
|
DataDir string `json:"dataDir"`
|
|
User *user.User `json:"user"`
|
|
Settings *models.Settings `json:"settings"`
|
|
Version string `json:"version"`
|
|
VersionName string `json:"versionName"`
|
|
ThemeSettings *models.Theme `json:"themeSettings"`
|
|
IsOffline bool `json:"isOffline"`
|
|
MediastreamSettings *models.MediastreamSettings `json:"mediastreamSettings"`
|
|
TorrentstreamSettings *models.TorrentstreamSettings `json:"torrentstreamSettings"`
|
|
DebridSettings *models.DebridSettings `json:"debridSettings"`
|
|
AnilistClientID string `json:"anilistClientId"`
|
|
Updating bool `json:"updating"` // If true, a new screen will be displayed
|
|
IsDesktopSidecar bool `json:"isDesktopSidecar"` // The server is running as a desktop sidecar
|
|
FeatureFlags core.FeatureFlags `json:"featureFlags"`
|
|
ServerReady bool `json:"serverReady"`
|
|
ServerHasPassword bool `json:"serverHasPassword"`
|
|
}
|
|
|
|
var clientInfoCache = result.NewResultMap[string, util.ClientInfo]()
|
|
|
|
// NewStatus returns a new Status struct.
|
|
// It uses the RouteCtx to get the App instance containing the Database instance.
|
|
func (h *Handler) NewStatus(c echo.Context) *Status {
|
|
var dbAcc *models.Account
|
|
var currentUser *user.User
|
|
var settings *models.Settings
|
|
var theme *models.Theme
|
|
//var mal *models.Mal
|
|
|
|
// Get the user from the database (if logged in)
|
|
if dbAcc, _ = h.App.Database.GetAccount(); dbAcc != nil {
|
|
currentUser, _ = user.NewUser(dbAcc)
|
|
if currentUser != nil {
|
|
currentUser.Token = "HIDDEN"
|
|
}
|
|
} else {
|
|
// If the user is not logged in, create a simulated user
|
|
currentUser = user.NewSimulatedUser()
|
|
}
|
|
|
|
if settings, _ = h.App.Database.GetSettings(); settings != nil {
|
|
if settings.ID == 0 || settings.Library == nil || settings.Torrent == nil || settings.MediaPlayer == nil {
|
|
settings = nil
|
|
}
|
|
}
|
|
|
|
clientInfo, found := clientInfoCache.Get(c.Request().UserAgent())
|
|
if !found {
|
|
clientInfo = util.GetClientInfo(c.Request().UserAgent())
|
|
clientInfoCache.Set(c.Request().UserAgent(), clientInfo)
|
|
}
|
|
|
|
theme, _ = h.App.Database.GetTheme()
|
|
|
|
status := &Status{
|
|
OS: runtime.GOOS,
|
|
ClientDevice: clientInfo.Device,
|
|
ClientPlatform: clientInfo.Platform,
|
|
DataDir: h.App.Config.Data.AppDataDir,
|
|
ClientUserAgent: c.Request().UserAgent(),
|
|
User: currentUser,
|
|
Settings: settings,
|
|
Version: h.App.Version,
|
|
VersionName: constants.VersionName,
|
|
ThemeSettings: theme,
|
|
IsOffline: h.App.Config.Server.Offline,
|
|
MediastreamSettings: h.App.SecondarySettings.Mediastream,
|
|
TorrentstreamSettings: h.App.SecondarySettings.Torrentstream,
|
|
DebridSettings: h.App.SecondarySettings.Debrid,
|
|
AnilistClientID: h.App.Config.Anilist.ClientID,
|
|
Updating: false,
|
|
IsDesktopSidecar: h.App.IsDesktopSidecar,
|
|
FeatureFlags: h.App.FeatureFlags,
|
|
ServerReady: h.App.ServerReady,
|
|
ServerHasPassword: h.App.Config.Server.Password != "",
|
|
}
|
|
|
|
if c.Get("unauthenticated") != nil && c.Get("unauthenticated").(bool) {
|
|
// If the user is unauthenticated, return a status with no user data
|
|
status.OS = ""
|
|
status.DataDir = ""
|
|
status.User = user.NewSimulatedUser()
|
|
status.ThemeSettings = nil
|
|
status.MediastreamSettings = nil
|
|
status.TorrentstreamSettings = nil
|
|
status.Settings = &models.Settings{}
|
|
status.DebridSettings = nil
|
|
status.FeatureFlags = core.FeatureFlags{}
|
|
}
|
|
|
|
return status
|
|
}
|
|
|
|
// HandleGetStatus
|
|
//
|
|
// @summary returns the server status.
|
|
// @desc The server status includes app info, auth info and settings.
|
|
// @desc The client uses this to set the UI.
|
|
// @desc It is called on every page load to get the most up-to-date data.
|
|
// @desc It should be called right after updating the settings.
|
|
// @route /api/v1/status [GET]
|
|
// @returns handlers.Status
|
|
func (h *Handler) HandleGetStatus(c echo.Context) error {
|
|
|
|
status := h.NewStatus(c)
|
|
|
|
return h.RespondWithData(c, status)
|
|
|
|
}
|
|
|
|
func (h *Handler) HandleGetLogContent(c echo.Context) error {
|
|
if h.App.Config == nil || h.App.Config.Logs.Dir == "" {
|
|
return h.RespondWithData(c, "")
|
|
}
|
|
|
|
filename := c.Param("*")
|
|
if filepath.Base(filename) != filename {
|
|
h.App.Logger.Error().Msg("handlers: Invalid filename")
|
|
return h.RespondWithError(c, fmt.Errorf("invalid filename"))
|
|
}
|
|
|
|
fp := filepath.Join(h.App.Config.Logs.Dir, filename)
|
|
|
|
if filepath.Ext(fp) != ".log" {
|
|
h.App.Logger.Error().Msg("handlers: Unsupported file extension")
|
|
return h.RespondWithError(c, fmt.Errorf("unsupported file extension"))
|
|
}
|
|
|
|
if _, err := os.Stat(fp); err != nil {
|
|
h.App.Logger.Error().Err(err).Msg("handlers: Stat error")
|
|
return h.RespondWithError(c, err)
|
|
}
|
|
|
|
contentB, err := os.ReadFile(fp)
|
|
if err != nil {
|
|
h.App.Logger.Error().Err(err).Msg("handlers: Failed to read log file")
|
|
return h.RespondWithError(c, err)
|
|
}
|
|
|
|
return h.RespondWithData(c, string(contentB))
|
|
}
|
|
|
|
var newestLogFilename = ""
|
|
|
|
// HandleGetLogFilenames
|
|
//
|
|
// @summary returns the log filenames.
|
|
// @desc This returns the filenames of all log files in the logs directory.
|
|
// @route /api/v1/logs/filenames [GET]
|
|
// @returns []string
|
|
func (h *Handler) HandleGetLogFilenames(c echo.Context) error {
|
|
if h.App.Config == nil || h.App.Config.Logs.Dir == "" {
|
|
return h.RespondWithData(c, []string{})
|
|
}
|
|
|
|
var filenames []string
|
|
filepath.WalkDir(h.App.Config.Logs.Dir, func(path string, d fs.DirEntry, err error) error {
|
|
if d.IsDir() {
|
|
return nil
|
|
}
|
|
|
|
if filepath.Ext(path) != ".log" {
|
|
return nil
|
|
}
|
|
|
|
filenames = append(filenames, filepath.Base(path))
|
|
return nil
|
|
})
|
|
|
|
// Sort from newest to oldest & store the newest log filename
|
|
if len(filenames) > 0 {
|
|
slices.SortStableFunc(filenames, func(i, j string) int {
|
|
return strings.Compare(j, i)
|
|
})
|
|
for _, filename := range filenames {
|
|
if strings.HasPrefix(strings.ToLower(filename), "seanime-") {
|
|
newestLogFilename = filename
|
|
break
|
|
}
|
|
}
|
|
}
|
|
|
|
return h.RespondWithData(c, filenames)
|
|
}
|
|
|
|
// HandleDeleteLogs
|
|
//
|
|
// @summary deletes certain log files.
|
|
// @desc This deletes the log files with the given filenames.
|
|
// @route /api/v1/logs [DELETE]
|
|
// @returns bool
|
|
func (h *Handler) HandleDeleteLogs(c echo.Context) error {
|
|
type body struct {
|
|
Filenames []string `json:"filenames"`
|
|
}
|
|
|
|
if h.App.Config == nil || h.App.Config.Logs.Dir == "" {
|
|
return h.RespondWithData(c, false)
|
|
}
|
|
|
|
var b body
|
|
if err := c.Bind(&b); err != nil {
|
|
return h.RespondWithError(c, err)
|
|
}
|
|
|
|
filepath.WalkDir(h.App.Config.Logs.Dir, func(path string, d fs.DirEntry, err error) error {
|
|
if d.IsDir() {
|
|
return nil
|
|
}
|
|
|
|
if filepath.Ext(path) != ".log" {
|
|
return nil
|
|
}
|
|
|
|
for _, filename := range b.Filenames {
|
|
if util.NormalizePath(filepath.Base(path)) == util.NormalizePath(filename) {
|
|
if util.NormalizePath(newestLogFilename) == util.NormalizePath(filename) {
|
|
return fmt.Errorf("cannot delete the newest log file")
|
|
}
|
|
if err := os.Remove(path); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
}
|
|
return nil
|
|
})
|
|
|
|
return h.RespondWithData(c, true)
|
|
}
|
|
|
|
// HandleGetLatestLogContent
|
|
//
|
|
// @summary returns the content of the latest server log file.
|
|
// @desc This returns the content of the most recent seanime- log file after flushing logs.
|
|
// @route /api/v1/logs/latest [GET]
|
|
// @returns string
|
|
func (h *Handler) HandleGetLatestLogContent(c echo.Context) error {
|
|
if h.App.Config == nil || h.App.Config.Logs.Dir == "" {
|
|
return h.RespondWithData(c, "")
|
|
}
|
|
|
|
// Flush logs first
|
|
if h.App.OnFlushLogs != nil {
|
|
h.App.OnFlushLogs()
|
|
// Small delay to ensure logs are written
|
|
time.Sleep(100 * time.Millisecond)
|
|
}
|
|
|
|
dirEntries, err := os.ReadDir(h.App.Config.Logs.Dir)
|
|
if err != nil {
|
|
h.App.Logger.Error().Err(err).Msg("handlers: Failed to read log directory")
|
|
return h.RespondWithError(c, err)
|
|
}
|
|
|
|
var logFiles []string
|
|
for _, entry := range dirEntries {
|
|
if entry.IsDir() {
|
|
continue
|
|
}
|
|
name := entry.Name()
|
|
if filepath.Ext(name) != ".log" || !strings.HasPrefix(strings.ToLower(name), "seanime-") {
|
|
continue
|
|
}
|
|
logFiles = append(logFiles, filepath.Join(h.App.Config.Logs.Dir, name))
|
|
}
|
|
|
|
if len(logFiles) == 0 {
|
|
h.App.Logger.Warn().Msg("handlers: No log files found")
|
|
return h.RespondWithData(c, "")
|
|
}
|
|
|
|
// Sort files in descending order based on filename
|
|
slices.SortFunc(logFiles, func(a, b string) int {
|
|
return strings.Compare(filepath.Base(b), filepath.Base(a))
|
|
})
|
|
|
|
latestLogFile := logFiles[0]
|
|
|
|
contentB, err := os.ReadFile(latestLogFile)
|
|
if err != nil {
|
|
h.App.Logger.Error().Err(err).Msg("handlers: Failed to read latest log file")
|
|
return h.RespondWithError(c, err)
|
|
}
|
|
|
|
return h.RespondWithData(c, string(contentB))
|
|
}
|
|
|
|
// HandleGetAnnouncements
|
|
//
|
|
// @summary returns the server announcements.
|
|
// @desc This returns the announcements for the server.
|
|
// @route /api/v1/announcements [POST]
|
|
// @returns []updater.Announcement
|
|
func (h *Handler) HandleGetAnnouncements(c echo.Context) error {
|
|
type body struct {
|
|
Platform string `json:"platform"`
|
|
}
|
|
|
|
var b body
|
|
if err := c.Bind(&b); err != nil {
|
|
return h.RespondWithError(c, err)
|
|
}
|
|
|
|
settings, _ := h.App.Database.GetSettings()
|
|
|
|
announcements := h.App.Updater.GetAnnouncements(h.App.Version, b.Platform, settings)
|
|
|
|
return h.RespondWithData(c, announcements)
|
|
|
|
}
|
|
|
|
type MemoryStatsResponse struct {
|
|
Alloc uint64 `json:"alloc"` // bytes allocated and not yet freed
|
|
TotalAlloc uint64 `json:"totalAlloc"` // bytes allocated (even if freed)
|
|
Sys uint64 `json:"sys"` // bytes obtained from system
|
|
Lookups uint64 `json:"lookups"` // number of pointer lookups
|
|
Mallocs uint64 `json:"mallocs"` // number of mallocs
|
|
Frees uint64 `json:"frees"` // number of frees
|
|
HeapAlloc uint64 `json:"heapAlloc"` // bytes allocated and not yet freed
|
|
HeapSys uint64 `json:"heapSys"` // bytes obtained from system
|
|
HeapIdle uint64 `json:"heapIdle"` // bytes in idle spans
|
|
HeapInuse uint64 `json:"heapInuse"` // bytes in non-idle span
|
|
HeapReleased uint64 `json:"heapReleased"` // bytes released to OS
|
|
HeapObjects uint64 `json:"heapObjects"` // total number of allocated objects
|
|
StackInuse uint64 `json:"stackInuse"` // bytes used by stack allocator
|
|
StackSys uint64 `json:"stackSys"` // bytes obtained from system for stack allocator
|
|
MSpanInuse uint64 `json:"mSpanInuse"` // bytes used by mspan structures
|
|
MSpanSys uint64 `json:"mSpanSys"` // bytes obtained from system for mspan structures
|
|
MCacheInuse uint64 `json:"mCacheInuse"` // bytes used by mcache structures
|
|
MCacheSys uint64 `json:"mCacheSys"` // bytes obtained from system for mcache structures
|
|
BuckHashSys uint64 `json:"buckHashSys"` // bytes used by the profiling bucket hash table
|
|
GCSys uint64 `json:"gcSys"` // bytes used for garbage collection system metadata
|
|
OtherSys uint64 `json:"otherSys"` // bytes used for other system allocations
|
|
NextGC uint64 `json:"nextGC"` // next collection will happen when HeapAlloc ≥ this amount
|
|
LastGC uint64 `json:"lastGC"` // time the last garbage collection finished
|
|
PauseTotalNs uint64 `json:"pauseTotalNs"` // cumulative nanoseconds in GC stop-the-world pauses
|
|
PauseNs uint64 `json:"pauseNs"` // nanoseconds in recent GC stop-the-world pause
|
|
NumGC uint32 `json:"numGC"` // number of completed GC cycles
|
|
NumForcedGC uint32 `json:"numForcedGC"` // number of GC cycles that were forced by the application calling the GC function
|
|
GCCPUFraction float64 `json:"gcCPUFraction"` // fraction of this program's available CPU time used by the GC since the program started
|
|
EnableGC bool `json:"enableGC"` // boolean that indicates GC is enabled
|
|
DebugGC bool `json:"debugGC"` // boolean that indicates GC debug mode is enabled
|
|
NumGoroutine int `json:"numGoroutine"` // number of goroutines
|
|
}
|
|
|
|
// HandleGetMemoryStats
|
|
//
|
|
// @summary returns current memory statistics.
|
|
// @desc This returns real-time memory usage statistics from the Go runtime.
|
|
// @route /api/v1/memory/stats [GET]
|
|
// @returns handlers.MemoryStatsResponse
|
|
func (h *Handler) HandleGetMemoryStats(c echo.Context) error {
|
|
var m runtime.MemStats
|
|
runtime.ReadMemStats(&m)
|
|
|
|
// Force garbage collection to get accurate memory stats
|
|
// runtime.GC()
|
|
runtime.ReadMemStats(&m)
|
|
|
|
response := MemoryStatsResponse{
|
|
Alloc: m.Alloc,
|
|
TotalAlloc: m.TotalAlloc,
|
|
Sys: m.Sys,
|
|
Lookups: m.Lookups,
|
|
Mallocs: m.Mallocs,
|
|
Frees: m.Frees,
|
|
HeapAlloc: m.HeapAlloc,
|
|
HeapSys: m.HeapSys,
|
|
HeapIdle: m.HeapIdle,
|
|
HeapInuse: m.HeapInuse,
|
|
HeapReleased: m.HeapReleased,
|
|
HeapObjects: m.HeapObjects,
|
|
StackInuse: m.StackInuse,
|
|
StackSys: m.StackSys,
|
|
MSpanInuse: m.MSpanInuse,
|
|
MSpanSys: m.MSpanSys,
|
|
MCacheInuse: m.MCacheInuse,
|
|
MCacheSys: m.MCacheSys,
|
|
BuckHashSys: m.BuckHashSys,
|
|
GCSys: m.GCSys,
|
|
OtherSys: m.OtherSys,
|
|
NextGC: m.NextGC,
|
|
LastGC: m.LastGC,
|
|
PauseTotalNs: m.PauseTotalNs,
|
|
PauseNs: m.PauseNs[0], // Most recent pause
|
|
NumGC: m.NumGC,
|
|
NumForcedGC: m.NumForcedGC,
|
|
GCCPUFraction: m.GCCPUFraction,
|
|
EnableGC: m.EnableGC,
|
|
DebugGC: m.DebugGC,
|
|
NumGoroutine: runtime.NumGoroutine(),
|
|
}
|
|
|
|
return h.RespondWithData(c, response)
|
|
}
|
|
|
|
// HandleGetMemoryProfile
|
|
//
|
|
// @summary generates and returns a memory profile.
|
|
// @desc This generates a memory profile that can be analyzed with go tool pprof.
|
|
// @desc Query parameters: heap=true for heap profile, allocs=true for alloc profile.
|
|
// @route /api/v1/memory/profile [GET]
|
|
// @returns nil
|
|
func (h *Handler) HandleGetMemoryProfile(c echo.Context) error {
|
|
// Parse query parameters
|
|
heap := c.QueryParam("heap") == "true"
|
|
allocs := c.QueryParam("allocs") == "true"
|
|
|
|
// Default to heap profile if no specific type requested
|
|
if !heap && !allocs {
|
|
heap = true
|
|
}
|
|
|
|
// Set response headers for file download
|
|
timestamp := time.Now().Format("2006-01-02_15-04-05")
|
|
var filename string
|
|
var profile *pprof.Profile
|
|
var err error
|
|
|
|
if heap {
|
|
filename = fmt.Sprintf("seanime-heap-profile-%s.pprof", timestamp)
|
|
profile = pprof.Lookup("heap")
|
|
} else if allocs {
|
|
filename = fmt.Sprintf("seanime-allocs-profile-%s.pprof", timestamp)
|
|
profile = pprof.Lookup("allocs")
|
|
}
|
|
|
|
if profile == nil {
|
|
h.App.Logger.Error().Msg("handlers: Failed to lookup memory profile")
|
|
return h.RespondWithError(c, fmt.Errorf("failed to lookup memory profile"))
|
|
}
|
|
|
|
c.Response().Header().Set("Content-Type", "application/octet-stream")
|
|
c.Response().Header().Set("Content-Disposition", fmt.Sprintf("attachment; filename=\"%s\"", filename))
|
|
|
|
// // Force garbage collection before profiling for more accurate results
|
|
// runtime.GC()
|
|
|
|
// Write profile to response
|
|
if err = profile.WriteTo(c.Response().Writer, 0); err != nil {
|
|
h.App.Logger.Error().Err(err).Msg("handlers: Failed to write memory profile")
|
|
return h.RespondWithError(c, err)
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// HandleGetGoRoutineProfile
|
|
//
|
|
// @summary generates and returns a goroutine profile.
|
|
// @desc This generates a goroutine profile showing all running goroutines and their stack traces.
|
|
// @route /api/v1/memory/goroutine [GET]
|
|
// @returns nil
|
|
func (h *Handler) HandleGetGoRoutineProfile(c echo.Context) error {
|
|
timestamp := time.Now().Format("2006-01-02_15-04-05")
|
|
filename := fmt.Sprintf("seanime-goroutine-profile-%s.pprof", timestamp)
|
|
|
|
profile := pprof.Lookup("goroutine")
|
|
if profile == nil {
|
|
h.App.Logger.Error().Msg("handlers: Failed to lookup goroutine profile")
|
|
return h.RespondWithError(c, fmt.Errorf("failed to lookup goroutine profile"))
|
|
}
|
|
|
|
c.Response().Header().Set("Content-Type", "application/octet-stream")
|
|
c.Response().Header().Set("Content-Disposition", fmt.Sprintf("attachment; filename=\"%s\"", filename))
|
|
|
|
if err := profile.WriteTo(c.Response().Writer, 0); err != nil {
|
|
h.App.Logger.Error().Err(err).Msg("handlers: Failed to write goroutine profile")
|
|
return h.RespondWithError(c, err)
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// HandleGetCPUProfile
|
|
//
|
|
// @summary generates and returns a CPU profile.
|
|
// @desc This generates a CPU profile for the specified duration (default 30 seconds).
|
|
// @desc Query parameter: duration=30 for duration in seconds.
|
|
// @route /api/v1/memory/cpu [GET]
|
|
// @returns nil
|
|
func (h *Handler) HandleGetCPUProfile(c echo.Context) error {
|
|
// Parse duration from query parameter (default to 30 seconds)
|
|
durationStr := c.QueryParam("duration")
|
|
duration := 30 * time.Second
|
|
if durationStr != "" {
|
|
if d, err := strconv.Atoi(durationStr); err == nil && d > 0 && d <= 300 { // Max 5 minutes
|
|
duration = time.Duration(d) * time.Second
|
|
}
|
|
}
|
|
|
|
timestamp := time.Now().Format("2006-01-02_15-04-05")
|
|
filename := fmt.Sprintf("seanime-cpu-profile-%s.pprof", timestamp)
|
|
|
|
c.Response().Header().Set("Content-Type", "application/octet-stream")
|
|
c.Response().Header().Set("Content-Disposition", fmt.Sprintf("attachment; filename=\"%s\"", filename))
|
|
|
|
// Start CPU profiling
|
|
if err := pprof.StartCPUProfile(c.Response().Writer); err != nil {
|
|
h.App.Logger.Error().Err(err).Msg("handlers: Failed to start CPU profile")
|
|
return h.RespondWithError(c, err)
|
|
}
|
|
|
|
// Profile for the specified duration
|
|
h.App.Logger.Info().Msgf("handlers: Starting CPU profile for %v", duration)
|
|
time.Sleep(duration)
|
|
|
|
// Stop CPU profiling
|
|
pprof.StopCPUProfile()
|
|
h.App.Logger.Info().Msg("handlers: CPU profile completed")
|
|
|
|
return nil
|
|
}
|
|
|
|
// HandleForceGC
|
|
//
|
|
// @summary forces garbage collection and returns memory stats.
|
|
// @desc This forces a garbage collection cycle and returns the updated memory statistics.
|
|
// @route /api/v1/memory/gc [POST]
|
|
// @returns handlers.MemoryStatsResponse
|
|
func (h *Handler) HandleForceGC(c echo.Context) error {
|
|
h.App.Logger.Info().Msg("handlers: Forcing garbage collection")
|
|
|
|
// Force garbage collection
|
|
runtime.GC()
|
|
runtime.GC() // Run twice to ensure cleanup
|
|
|
|
// Get updated memory stats
|
|
var m runtime.MemStats
|
|
runtime.ReadMemStats(&m)
|
|
|
|
response := MemoryStatsResponse{
|
|
Alloc: m.Alloc,
|
|
TotalAlloc: m.TotalAlloc,
|
|
Sys: m.Sys,
|
|
Lookups: m.Lookups,
|
|
Mallocs: m.Mallocs,
|
|
Frees: m.Frees,
|
|
HeapAlloc: m.HeapAlloc,
|
|
HeapSys: m.HeapSys,
|
|
HeapIdle: m.HeapIdle,
|
|
HeapInuse: m.HeapInuse,
|
|
HeapReleased: m.HeapReleased,
|
|
HeapObjects: m.HeapObjects,
|
|
StackInuse: m.StackInuse,
|
|
StackSys: m.StackSys,
|
|
MSpanInuse: m.MSpanInuse,
|
|
MSpanSys: m.MSpanSys,
|
|
MCacheInuse: m.MCacheInuse,
|
|
MCacheSys: m.MCacheSys,
|
|
BuckHashSys: m.BuckHashSys,
|
|
GCSys: m.GCSys,
|
|
OtherSys: m.OtherSys,
|
|
NextGC: m.NextGC,
|
|
LastGC: m.LastGC,
|
|
PauseTotalNs: m.PauseTotalNs,
|
|
PauseNs: m.PauseNs[0],
|
|
NumGC: m.NumGC,
|
|
NumForcedGC: m.NumForcedGC,
|
|
GCCPUFraction: m.GCCPUFraction,
|
|
EnableGC: m.EnableGC,
|
|
DebugGC: m.DebugGC,
|
|
NumGoroutine: runtime.NumGoroutine(),
|
|
}
|
|
|
|
h.App.Logger.Info().Msgf("handlers: GC completed, heap size: %d bytes", response.HeapAlloc)
|
|
|
|
return h.RespondWithData(c, response)
|
|
}
|