mirror of
https://github.com/DeBrosOfficial/orama.git
synced 2026-03-17 20:46:58 +00:00
201 lines
5.0 KiB
Go
201 lines
5.0 KiB
Go
package report
|
|
|
|
import (
|
|
"context"
|
|
"path/filepath"
|
|
"strconv"
|
|
"strings"
|
|
"time"
|
|
)
|
|
|
|
var coreServices = []string{
|
|
"orama-node",
|
|
"orama-olric",
|
|
"orama-ipfs",
|
|
"orama-ipfs-cluster",
|
|
"orama-anyone-relay",
|
|
"orama-anyone-client",
|
|
"coredns",
|
|
"caddy",
|
|
"wg-quick@wg0",
|
|
}
|
|
|
|
func collectServices() *ServicesReport {
|
|
ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
|
|
defer cancel()
|
|
|
|
report := &ServicesReport{}
|
|
|
|
// Collect core services.
|
|
for _, name := range coreServices {
|
|
info := collectServiceInfo(ctx, name)
|
|
report.Services = append(report.Services, info)
|
|
}
|
|
|
|
// Discover namespace services (orama-deploy-*.service).
|
|
nsServices := discoverNamespaceServices()
|
|
for _, name := range nsServices {
|
|
info := collectServiceInfo(ctx, name)
|
|
report.Services = append(report.Services, info)
|
|
}
|
|
|
|
// Collect failed units.
|
|
report.FailedUnits = collectFailedUnits(ctx)
|
|
|
|
return report
|
|
}
|
|
|
|
func collectServiceInfo(ctx context.Context, name string) ServiceInfo {
|
|
info := ServiceInfo{Name: name}
|
|
|
|
// Get all properties in a single systemctl show call.
|
|
out, err := runCmd(ctx, "systemctl", "show", name,
|
|
"--property=ActiveState,SubState,NRestarts,ActiveEnterTimestamp,MemoryCurrent,CPUUsageNSec,MainPID")
|
|
if err != nil {
|
|
info.ActiveState = "unknown"
|
|
info.SubState = "unknown"
|
|
return info
|
|
}
|
|
|
|
props := parseProperties(out)
|
|
|
|
info.ActiveState = props["ActiveState"]
|
|
info.SubState = props["SubState"]
|
|
info.NRestarts = parseInt(props["NRestarts"])
|
|
info.MainPID = parseInt(props["MainPID"])
|
|
info.MemoryCurrentMB = parseMemoryMB(props["MemoryCurrent"])
|
|
info.CPUUsageNSec = parseInt64(props["CPUUsageNSec"])
|
|
|
|
// Calculate uptime from ActiveEnterTimestamp.
|
|
if ts := props["ActiveEnterTimestamp"]; ts != "" && ts != "n/a" {
|
|
info.ActiveSinceSec = parseActiveSince(ts)
|
|
}
|
|
|
|
// Check if service is enabled.
|
|
enabledOut, err := runCmd(ctx, "systemctl", "is-enabled", name)
|
|
if err == nil && strings.TrimSpace(enabledOut) == "enabled" {
|
|
info.Enabled = true
|
|
}
|
|
|
|
// Restart loop detection: restarted more than 3 times and running for less than 5 minutes.
|
|
info.RestartLoopRisk = info.NRestarts > 3 && info.ActiveSinceSec > 0 && info.ActiveSinceSec < 300
|
|
|
|
return info
|
|
}
|
|
|
|
// parseProperties parses "Key=Value" lines from systemctl show output into a map.
|
|
func parseProperties(output string) map[string]string {
|
|
props := make(map[string]string)
|
|
for _, line := range strings.Split(output, "\n") {
|
|
line = strings.TrimSpace(line)
|
|
if line == "" {
|
|
continue
|
|
}
|
|
idx := strings.IndexByte(line, '=')
|
|
if idx < 0 {
|
|
continue
|
|
}
|
|
key := line[:idx]
|
|
value := line[idx+1:]
|
|
props[key] = value
|
|
}
|
|
return props
|
|
}
|
|
|
|
// parseMemoryMB converts a MemoryCurrent value (bytes as uint64, "[not set]", or "infinity") to MB.
|
|
func parseMemoryMB(s string) int {
|
|
s = strings.TrimSpace(s)
|
|
if s == "" || s == "[not set]" || s == "infinity" {
|
|
return 0
|
|
}
|
|
bytes, err := strconv.ParseUint(s, 10, 64)
|
|
if err != nil {
|
|
return 0
|
|
}
|
|
return int(bytes / (1024 * 1024))
|
|
}
|
|
|
|
// parseActiveSince parses an ActiveEnterTimestamp like "Fri 2024-01-05 10:30:00 UTC"
|
|
// and returns the number of seconds elapsed since that time.
|
|
func parseActiveSince(ts string) int64 {
|
|
// systemctl outputs timestamps in the form: "Day YYYY-MM-DD HH:MM:SS TZ"
|
|
// e.g. "Fri 2024-01-05 10:30:00 UTC"
|
|
layouts := []string{
|
|
"Mon 2006-01-02 15:04:05 MST",
|
|
"Mon 2006-01-02 15:04:05 -0700",
|
|
}
|
|
ts = strings.TrimSpace(ts)
|
|
for _, layout := range layouts {
|
|
t, err := time.Parse(layout, ts)
|
|
if err == nil {
|
|
sec := int64(time.Since(t).Seconds())
|
|
if sec < 0 {
|
|
return 0
|
|
}
|
|
return sec
|
|
}
|
|
}
|
|
return 0
|
|
}
|
|
|
|
func parseInt(s string) int {
|
|
s = strings.TrimSpace(s)
|
|
if s == "" || s == "[not set]" {
|
|
return 0
|
|
}
|
|
v, _ := strconv.Atoi(s)
|
|
return v
|
|
}
|
|
|
|
func parseInt64(s string) int64 {
|
|
s = strings.TrimSpace(s)
|
|
if s == "" || s == "[not set]" {
|
|
return 0
|
|
}
|
|
v, _ := strconv.ParseInt(s, 10, 64)
|
|
return v
|
|
}
|
|
|
|
// collectFailedUnits runs `systemctl --failed` and extracts unit names from the first column.
|
|
func collectFailedUnits(ctx context.Context) []string {
|
|
out, err := runCmd(ctx, "systemctl", "--failed", "--no-legend", "--no-pager")
|
|
if err != nil {
|
|
return nil
|
|
}
|
|
|
|
var units []string
|
|
for _, line := range strings.Split(out, "\n") {
|
|
line = strings.TrimSpace(line)
|
|
if line == "" {
|
|
continue
|
|
}
|
|
fields := strings.Fields(line)
|
|
if len(fields) > 0 {
|
|
// First column may have a bullet prefix; strip common markers.
|
|
unit := strings.TrimLeft(fields[0], "●* ")
|
|
if unit != "" {
|
|
units = append(units, unit)
|
|
}
|
|
}
|
|
}
|
|
return units
|
|
}
|
|
|
|
// discoverNamespaceServices finds orama-namespace-*@*.service files in /etc/systemd/system
|
|
// and returns the service names (without the .service suffix path).
|
|
func discoverNamespaceServices() []string {
|
|
matches, err := filepath.Glob("/etc/systemd/system/orama-namespace-*@*.service")
|
|
if err != nil || len(matches) == 0 {
|
|
return nil
|
|
}
|
|
|
|
var services []string
|
|
for _, path := range matches {
|
|
base := filepath.Base(path)
|
|
// Strip the .service suffix to get the unit name.
|
|
name := strings.TrimSuffix(base, ".service")
|
|
services = append(services, name)
|
|
}
|
|
return services
|
|
}
|