package report import ( "context" "encoding/json" "fmt" "io" "net/http" "os" "path/filepath" "regexp" "strconv" "strings" "time" ) // collectNamespaces discovers deployed namespaces and checks health of their // per-namespace services (RQLite, Olric, Gateway). func collectNamespaces() []NamespaceReport { namespaces := discoverNamespaces() if len(namespaces) == 0 { return nil } var reports []NamespaceReport for _, ns := range namespaces { reports = append(reports, collectNamespaceReport(ns)) } return reports } type nsInfo struct { name string portBase int } // discoverNamespaces finds deployed namespaces by looking for systemd service units // and/or the filesystem namespace directory. func discoverNamespaces() []nsInfo { var result []nsInfo seen := make(map[string]bool) // Strategy 1: Glob for orama-namespace-rqlite@*.service files. matches, _ := filepath.Glob("/etc/systemd/system/orama-namespace-rqlite@*.service") for _, path := range matches { base := filepath.Base(path) // Extract namespace name: orama-namespace-rqlite@.service name := strings.TrimPrefix(base, "orama-namespace-rqlite@") name = strings.TrimSuffix(name, ".service") if name == "" || seen[name] { continue } seen[name] = true portBase := parsePortFromEnvFile(name) if portBase > 0 { result = append(result, nsInfo{name: name, portBase: portBase}) } } // Strategy 2: Check filesystem for any namespaces not found via systemd. nsDir := "/opt/orama/.orama/data/namespaces" entries, err := os.ReadDir(nsDir) if err == nil { for _, entry := range entries { if !entry.IsDir() || seen[entry.Name()] { continue } name := entry.Name() seen[name] = true portBase := parsePortFromEnvFile(name) if portBase > 0 { result = append(result, nsInfo{name: name, portBase: portBase}) } } } return result } // parsePortFromEnvFile reads the RQLite env file for a namespace and extracts // the HTTP port from HTTP_ADDR (e.g. "0.0.0.0:14001"). func parsePortFromEnvFile(namespace string) int { envPath := fmt.Sprintf("/opt/orama/.orama/data/namespaces/%s/rqlite.env", namespace) data, err := os.ReadFile(envPath) if err != nil { return 0 } httpAddrRe := regexp.MustCompile(`HTTP_ADDR=\S+:(\d+)`) if m := httpAddrRe.FindStringSubmatch(string(data)); len(m) >= 2 { if port, err := strconv.Atoi(m[1]); err == nil { return port } } return 0 } // collectNamespaceReport checks the health of services for a single namespace. func collectNamespaceReport(ns nsInfo) NamespaceReport { r := NamespaceReport{ Name: ns.name, PortBase: ns.portBase, } // 1. RQLiteUp + RQLiteState: GET http://localhost:/status { ctx, cancel := context.WithTimeout(context.Background(), 3*time.Second) defer cancel() url := fmt.Sprintf("http://localhost:%d/status", ns.portBase) if body, err := httpGet(ctx, url); err == nil { r.RQLiteUp = true var status map[string]interface{} if err := json.Unmarshal(body, &status); err == nil { r.RQLiteState = getNestedString(status, "store", "raft", "state") } } } // 2. RQLiteReady: GET http://localhost:/readyz { ctx, cancel := context.WithTimeout(context.Background(), 3*time.Second) defer cancel() url := fmt.Sprintf("http://localhost:%d/readyz", ns.portBase) req, err := http.NewRequestWithContext(ctx, http.MethodGet, url, nil) if err == nil { if resp, err := http.DefaultClient.Do(req); err == nil { io.Copy(io.Discard, resp.Body) resp.Body.Close() r.RQLiteReady = resp.StatusCode == http.StatusOK } } } // 3. OlricUp: check if port_base+2 is listening { ctx, cancel := context.WithTimeout(context.Background(), 4*time.Second) defer cancel() if out, err := runCmd(ctx, "ss", "-tlnp"); err == nil { r.OlricUp = portIsListening(out, ns.portBase+2) } } // 4. GatewayUp + GatewayStatus: GET http://localhost:/v1/health { ctx, cancel := context.WithTimeout(context.Background(), 3*time.Second) defer cancel() url := fmt.Sprintf("http://localhost:%d/v1/health", ns.portBase+4) req, err := http.NewRequestWithContext(ctx, http.MethodGet, url, nil) if err == nil { if resp, err := http.DefaultClient.Do(req); err == nil { io.Copy(io.Discard, resp.Body) resp.Body.Close() r.GatewayUp = true r.GatewayStatus = resp.StatusCode } } } return r }