This commit is contained in:
ponzischeme89
2026-08-19 14:25:44 +12:00
parent 2b43b9ef12
commit 590e069366
83 changed files with 8948 additions and 1266 deletions
+43 -28
View File
@@ -5,10 +5,8 @@ import (
"crypto/subtle"
_ "embed"
"encoding/json"
"io"
"net/http"
"os"
"runtime"
"runtime/debug"
"strconv"
"strings"
"time"
@@ -16,6 +14,7 @@ import (
"github.com/ponzischeme89/memby/server/internal/appupdate"
"github.com/ponzischeme89/memby/server/internal/buildinfo"
"github.com/ponzischeme89/memby/server/internal/library"
"github.com/ponzischeme89/memby/server/internal/runtimestats"
"github.com/ponzischeme89/memby/server/internal/store"
)
@@ -54,6 +53,9 @@ func (s *Server) adminRoutes() http.Handler {
mux.Handle("GET /admin/api/searches", s.adminAuth(s.handleAdminSearches))
mux.Handle("GET /admin/api/events", s.adminAuth(s.handleAdminEvents))
mux.Handle("GET /admin/api/runtime", s.adminAuth(s.handleAdminRuntime))
// The breakdown is its own route because it costs its own money: walking every
// goroutine's stack stops the world, so it is asked for rather than polled.
mux.Handle("GET /admin/api/runtime/goroutines", s.adminAuth(s.handleAdminGoroutines))
mux.Handle("POST /admin/api/sync", s.adminAuth(s.handleAdminSync))
mux.Handle("GET /admin/api/ingest", s.adminAuth(s.handleAdminIngest))
mux.Handle("POST /admin/api/for-you", s.adminAuth(s.handleAdminForYou))
@@ -101,6 +103,18 @@ func (s *Server) adminRoutes() http.Handler {
// abandoned tab's session alive.
mux.Handle("GET /admin/api/notifications/stream", s.adminAuth(s.handleAdminNotificationStream))
// The Integrations area. Deliberately a longer path than /admin/api/integrations,
// which is the older, narrower question — the webhook destinations administrative
// events are posted to. These are the external services Memby depends on.
mux.Handle("GET /admin/api/integrations/services",
s.adminAuth(s.handleAdminIntegrationServices))
mux.Handle("GET /admin/api/integrations/services/{integrationID}",
s.adminAuth(s.handleAdminIntegrationService))
mux.Handle("POST /admin/api/integrations/services/{integrationID}/enabled",
s.adminAuth(s.handleAdminIntegrationEnabled))
mux.Handle("POST /admin/api/integrations/services/{integrationID}/test",
s.adminAuth(s.handleAdminIntegrationTest))
mux.Handle("GET /admin/api/integrations", s.adminAuth(s.handleAdminIntegrations))
mux.Handle("POST /admin/api/integrations", s.adminAuth(s.handleAdminSaveIntegration))
mux.Handle("DELETE /admin/api/integrations/{integrationID}", s.adminAuth(s.handleAdminDeleteIntegration))
@@ -134,33 +148,34 @@ func (s *Server) adminRoutes() http.Handler {
return mux
}
type adminRuntimeStatus struct {
Goroutines int `json:"goroutines"`
GOMAXPROCS int `json:"gomaxprocs"`
HeapAlloc uint64 `json:"heapAlloc"`
HeapInuse uint64 `json:"heapInuse"`
HeapIdle uint64 `json:"heapIdle"`
HeapReleased uint64 `json:"heapReleased"`
StackInuse uint64 `json:"stackInuse"`
Sys uint64 `json:"sys"`
NextGC uint64 `json:"nextGc"`
NumGC uint32 `json:"numGc"`
MemoryLimit int64 `json:"memoryLimit"`
ConfiguredLim string `json:"configuredLimit,omitempty"`
// The console's Process card used to be handed a bare goroutine count, which is a number
// nobody can act on: it says nothing about what those goroutines are doing, which part of
// Memby they belong to, or whether it has been climbing all week. Everything behind these
// two routes lives in internal/runtimestats, including the health verdict, so the
// thresholds and the sentences explaining them sit beside each other and can be tested.
func (s *Server) handleAdminRuntime(w http.ResponseWriter, _ *http.Request) {
w.Header().Set("Cache-Control", "no-store")
writeJSON(w, http.StatusOK, runtimestats.Read())
}
func (s *Server) handleAdminRuntime(w http.ResponseWriter, _ *http.Request) {
var memory runtime.MemStats
runtime.ReadMemStats(&memory)
// handleAdminGoroutines is the on-demand snapshot. `?format=text` returns the raw dump for
// an operator who wants the whole thing — which is the alternative to leaving a profiling
// endpoint permanently mounted, and unlike one it is behind the admin guard and produces
// nothing until somebody asks.
func (s *Server) handleAdminGoroutines(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Cache-Control", "no-store")
writeJSON(w, http.StatusOK, adminRuntimeStatus{
Goroutines: runtime.NumGoroutine(), GOMAXPROCS: runtime.GOMAXPROCS(0),
HeapAlloc: memory.HeapAlloc, HeapInuse: memory.HeapInuse,
HeapIdle: memory.HeapIdle, HeapReleased: memory.HeapReleased,
StackInuse: memory.StackInuse, Sys: memory.Sys,
NextGC: memory.NextGC, NumGC: memory.NumGC,
MemoryLimit: debug.SetMemoryLimit(-1), ConfiguredLim: os.Getenv("GOMEMLIMIT"),
})
log := s.loggerFor(r.Context())
if r.URL.Query().Get("format") == "text" {
log.Info("goroutine dump requested", "component", "admin")
w.Header().Set("Content-Type", "text/plain; charset=utf-8")
w.Header().Set("Content-Disposition", "attachment; filename=memby-goroutines.txt")
_, _ = io.WriteString(w, runtimestats.StackDump())
return
}
report := runtimestats.CollectGoroutines()
log.Info("goroutine breakdown collected",
"component", "admin", "goroutines", report.Total, "took_ms", report.CollectedInMs)
writeJSON(w, http.StatusOK, report)
}
func (s *Server) handleAdminEvents(w http.ResponseWriter, r *http.Request) {
@@ -686,7 +701,7 @@ func (s *Server) handleAdminForYou(w http.ResponseWriter, r *http.Request) {
return
}
}
if err := s.forYou.RebuildAll(ctx, true); err != nil {
if _, err := s.forYou.RebuildAll(ctx, true); err != nil {
s.log.Error("manual For You rebuild failed",
"component", "admin", "action", req.Action, "error", err)
}