diff --git a/cmd/ingestor/decoder.go b/cmd/ingestor/decoder.go index a7e309fb..e2dfd43e 100644 --- a/cmd/ingestor/decoder.go +++ b/cmd/ingestor/decoder.go @@ -251,8 +251,8 @@ func decodeAdvert(buf []byte) Payload { } if p.Flags.HasName { name := string(appdata[off:]) - // Trim trailing null bytes name = strings.TrimRight(name, "\x00") + name = sanitizeName(name) p.Name = name } } @@ -605,6 +605,18 @@ func ValidateAdvert(p *Payload) (bool, string) { return true, "" } +// sanitizeName strips non-printable characters (< 0x20 except tab/newline) and DEL. +func sanitizeName(s string) string { + var b strings.Builder + b.Grow(len(s)) + for _, c := range s { + if c == '\t' || c == '\n' || (c >= 0x20 && c != 0x7f) { + b.WriteRune(c) + } + } + return b.String() +} + func advertRole(f *AdvertFlags) string { if f.Repeater { return "repeater" diff --git a/cmd/server/db.go b/cmd/server/db.go index 9757628b..cd174a8c 100644 --- a/cmd/server/db.go +++ b/cmd/server/db.go @@ -189,6 +189,7 @@ type Stats struct { TotalNodesAllTime int `json:"totalNodesAllTime"` TotalObservers int `json:"totalObservers"` PacketsLastHour int `json:"packetsLastHour"` + PacketsLast24h int `json:"packetsLast24h"` } // GetStats returns aggregate counts (matches Node.js db.getStats shape). @@ -210,6 +211,9 @@ func (db *DB) GetStats() (*Stats, error) { oneHourAgo := time.Now().Add(-1 * time.Hour).Unix() db.conn.QueryRow("SELECT COUNT(*) FROM observations WHERE timestamp > ?", oneHourAgo).Scan(&s.PacketsLastHour) + oneDayAgo := time.Now().Add(-24 * time.Hour).Unix() + db.conn.QueryRow("SELECT COUNT(*) FROM observations WHERE timestamp > ?", oneDayAgo).Scan(&s.PacketsLast24h) + return s, nil } diff --git a/cmd/server/decoder.go b/cmd/server/decoder.go index 3aac41d1..fd51a7e7 100644 --- a/cmd/server/decoder.go +++ b/cmd/server/decoder.go @@ -229,6 +229,7 @@ func decodeAdvert(buf []byte) Payload { if p.Flags.HasName { name := string(appdata[off:]) name = strings.TrimRight(name, "\x00") + name = sanitizeName(name) p.Name = name } } @@ -457,6 +458,18 @@ func ValidateAdvert(p *Payload) (bool, string) { return true, "" } +// sanitizeName strips non-printable characters (< 0x20 except tab/newline) and DEL. +func sanitizeName(s string) string { + var b strings.Builder + b.Grow(len(s)) + for _, c := range s { + if c == '\t' || c == '\n' || (c >= 0x20 && c != 0x7f) { + b.WriteRune(c) + } + } + return b.String() +} + func advertRole(f *AdvertFlags) string { if f.Repeater { return "repeater" diff --git a/cmd/server/routes.go b/cmd/server/routes.go index f063381c..3a94c18d 100644 --- a/cmd/server/routes.go +++ b/cmd/server/routes.go @@ -11,6 +11,7 @@ import ( "sort" "strconv" "strings" + "sync" "time" "github.com/gorilla/mux" @@ -27,6 +28,11 @@ type Server struct { version string commit string buildTime string + + // Cached runtime.MemStats to avoid stop-the-world pauses on every health check + memStatsMu sync.Mutex + memStatsCache runtime.MemStats + memStatsCachedAt time.Time } // PerfStats tracks request performance. @@ -66,6 +72,20 @@ func NewServer(db *DB, cfg *Config, hub *Hub) *Server { } } +const memStatsTTL = 5 * time.Second + +// getMemStats returns cached runtime.MemStats, refreshing at most every 5 seconds. +// runtime.ReadMemStats() stops the world; caching prevents per-request GC pauses. +func (s *Server) getMemStats() runtime.MemStats { + s.memStatsMu.Lock() + defer s.memStatsMu.Unlock() + if time.Since(s.memStatsCachedAt) > memStatsTTL { + runtime.ReadMemStats(&s.memStatsCache) + s.memStatsCachedAt = time.Now() + } + return s.memStatsCache +} + // RegisterRoutes sets up all HTTP routes on the given router. func (s *Server) RegisterRoutes(r *mux.Router) { // Performance instrumentation middleware @@ -274,8 +294,7 @@ func (s *Server) handleConfigMap(w http.ResponseWriter, r *http.Request) { // --- System Handlers --- func (s *Server) handleHealth(w http.ResponseWriter, r *http.Request) { - var m runtime.MemStats - runtime.ReadMemStats(&m) + m := s.getMemStats() uptime := time.Since(s.startedAt).Seconds() wsClients := 0 @@ -381,6 +400,7 @@ func (s *Server) handleStats(w http.ResponseWriter, r *http.Request) { TotalNodesAllTime: stats.TotalNodesAllTime, TotalObservers: stats.TotalObservers, PacketsLastHour: stats.PacketsLastHour, + PacketsLast24h: stats.PacketsLast24h, Engine: "go", Version: s.version, Commit: s.commit, diff --git a/cmd/server/store.go b/cmd/server/store.go index 13a84ede..c6c3882b 100644 --- a/cmd/server/store.go +++ b/cmd/server/store.go @@ -440,6 +440,9 @@ func (s *PacketStore) GetStoreStats() (*Stats, error) { oneHourAgo := time.Now().Add(-1 * time.Hour).Unix() s.db.conn.QueryRow("SELECT COUNT(*) FROM observations WHERE timestamp > ?", oneHourAgo).Scan(&st.PacketsLastHour) + oneDayAgo := time.Now().Add(-24 * time.Hour).Unix() + s.db.conn.QueryRow("SELECT COUNT(*) FROM observations WHERE timestamp > ?", oneDayAgo).Scan(&st.PacketsLast24h) + return st, nil } diff --git a/cmd/server/testdata/golden/shapes.json b/cmd/server/testdata/golden/shapes.json index b33fb71d..c04e4e5e 100644 --- a/cmd/server/testdata/golden/shapes.json +++ b/cmd/server/testdata/golden/shapes.json @@ -1506,6 +1506,9 @@ "packetsLastHour": { "type": "number" }, + "packetsLast24h": { + "type": "number" + }, "counts": { "type": "object", "keys": { diff --git a/cmd/server/types.go b/cmd/server/types.go index 6a8978a9..7efe1760 100644 --- a/cmd/server/types.go +++ b/cmd/server/types.go @@ -62,6 +62,7 @@ type StatsResponse struct { TotalNodesAllTime int `json:"totalNodesAllTime"` TotalObservers int `json:"totalObservers"` PacketsLastHour int `json:"packetsLastHour"` + PacketsLast24h int `json:"packetsLast24h"` Engine string `json:"engine"` Version string `json:"version"` Commit string `json:"commit"` diff --git a/db.js b/db.js index bd872693..4c36bb77 100644 --- a/db.js +++ b/db.js @@ -595,6 +595,7 @@ function getStats() { totalNodesAllTime: stmts.countNodes.get().count, totalObservers: stmts.countObservers.get().count, packetsLastHour: stmts.countRecentPackets.get(oneHourAgo).count, + packetsLast24h: stmts.countRecentPackets.get(new Date(Date.now() - 24 * 3600000).toISOString()).count, }; } diff --git a/decoder.js b/decoder.js index 355d34fb..104f5a2a 100644 --- a/decoder.js +++ b/decoder.js @@ -135,7 +135,10 @@ function decodeAdvert(buf) { off += 8; } if (result.flags.hasName) { - result.name = appdata.subarray(off).toString('utf8'); + let name = appdata.subarray(off).toString('utf8'); + // Strip non-printable characters (< 0x20 except tab/newline) and DEL + name = name.replace(/[\x00-\x08\x0b\x0c\x0e-\x1f\x7f]/g, ''); + result.name = name; } }