node build fixed
This commit is contained in:
599
seanime-2.9.10/internal/handlers/status.go
Normal file
599
seanime-2.9.10/internal/handlers/status.go
Normal file
@@ -0,0 +1,599 @@
|
||||
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)
|
||||
}
|
||||
Reference in New Issue
Block a user