diff --git a/CHANGELOG.md b/CHANGELOG.md index d0984cb24..34ff513f7 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,6 +8,7 @@ NOTE: all releases may include dependency updates, not specifically mentioned - feat: integrate try-as library [#912](https://github.com/hypermodeinc/modus/pull/912) - fix: check topic actor status before publishing events [#918](https://github.com/hypermodeinc/modus/pull/918) +- feat: update health endpoint [#924](https://github.com/hypermodeinc/modus/pull/924) ## 2025-06-25 - Runtime v0.18.2 diff --git a/runtime/actors/agents.go b/runtime/actors/agents.go index 1cb6bf1af..33b908cab 100644 --- a/runtime/actors/agents.go +++ b/runtime/actors/agents.go @@ -323,27 +323,3 @@ func ListActiveAgents(ctx context.Context) ([]AgentInfo, error) { return results, nil } - -func ListLocalAgents(ctx context.Context) []AgentInfo { - if _actorSystem == nil { - return nil - } - - span, _ := utils.NewSentrySpanForCurrentFunc(ctx) - defer span.Finish() - - actors := _actorSystem.Actors() - results := make([]AgentInfo, 0, len(actors)) - - for _, pid := range actors { - if actor, ok := pid.Actor().(*wasmAgentActor); ok { - results = append(results, AgentInfo{ - Id: actor.agentId, - Name: actor.agentName, - Status: actor.status, - }) - } - } - - return results -} diff --git a/runtime/app/app.go b/runtime/app/app.go index 44820f42c..41b0007b3 100644 --- a/runtime/app/app.go +++ b/runtime/app/app.go @@ -97,6 +97,10 @@ func ModusHomeDir() string { } func KubernetesNamespace() (string, bool) { + return kubernetesNamespace() +} + +var kubernetesNamespace = sync.OnceValues(func() (string, bool) { if ns := os.Getenv("NAMESPACE"); ns != "" { return ns, true } @@ -104,4 +108,4 @@ func KubernetesNamespace() (string, bool) { return strings.TrimSpace(string(data)), true } return "", false -} +}) diff --git a/runtime/httpserver/health.go b/runtime/httpserver/health.go index 046fcf5cf..78cb337ed 100644 --- a/runtime/httpserver/health.go +++ b/runtime/httpserver/health.go @@ -10,43 +10,34 @@ package httpserver import ( - "encoding/json" - "fmt" "net/http" + "runtime" - "github.com/hypermodeinc/modus/runtime/actors" "github.com/hypermodeinc/modus/runtime/app" + "github.com/hypermodeinc/modus/runtime/logger" + "github.com/hypermodeinc/modus/runtime/utils" ) var healthHandler = http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - env := app.Config().Environment() - ver := app.VersionNumber() - agents := actors.ListLocalAgents(r.Context()) - w.Header().Set("Content-Type", "application/json") - w.WriteHeader(http.StatusOK) - - // custom format the JSON response for easy readability - _, _ = w.Write([]byte(`{ - "status": "ok", - "environment": "` + env + `", - "version": "` + ver + `", -`)) - - if len(agents) == 0 { - _, _ = w.Write([]byte(` "agents": []` + "\n")) - } else { - _, _ = w.Write([]byte(` "agents": [` + "\n")) - for i, agent := range agents { - if i > 0 { - _, _ = w.Write([]byte(",\n")) - } - name, _ := json.Marshal(agent.Name) - _, _ = w.Write(fmt.Appendf(nil, ` {"id": "%s", "name": %s, "status": "%s"}`, agent.Id, name, agent.Status)) - } - _, _ = w.Write([]byte("\n ]\n")) + data := []utils.KeyValuePair{ + {Key: "status", Value: "ok"}, + {Key: "environment", Value: app.Config().Environment()}, + {Key: "app_version", Value: app.VersionNumber()}, + {Key: "go_version", Value: runtime.Version()}, + } + if ns, ok := app.KubernetesNamespace(); ok { + data = append(data, utils.KeyValuePair{Key: "kubernetes_namespace", Value: ns}) } - _, _ = w.Write([]byte("}\n")) + jsonBytes, err := utils.MakeJsonObject(data, true) + if err != nil { + logger.Err(r.Context(), err).Msg("Failed to serialize health check response.") + w.WriteHeader(http.StatusInternalServerError) + return + } + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(http.StatusOK) + _, _ = w.Write(jsonBytes) }) diff --git a/runtime/utils/json.go b/runtime/utils/json.go index e0a82b035..40bcb3361 100644 --- a/runtime/utils/json.go +++ b/runtime/utils/json.go @@ -31,3 +31,59 @@ func JsonDeserialize(data []byte, v any) error { dec.UseNumber() return dec.Decode(v) } + +type KeyValuePair struct { + Key string + Value any +} + +// MakeJsonObject creates a JSON object from the given key-value pairs. +func MakeJsonObject(pairs []KeyValuePair, pretty bool) ([]byte, error) { + var buf bytes.Buffer + + if pretty { + buf.WriteString("{\n") + for i, kv := range pairs { + keyBytes, err := json.Marshal(kv.Key) + if err != nil { + return nil, err + } + valBytes, err := json.Marshal(kv.Value) + if err != nil { + return nil, err + } + + buf.WriteString(" ") + buf.Write(keyBytes) + buf.WriteString(": ") + buf.Write(valBytes) + if i < len(pairs)-1 { + buf.WriteByte(',') + } + buf.WriteByte('\n') + } + buf.WriteString("}\n") + } else { + buf.WriteByte('{') + for i, kv := range pairs { + keyBytes, err := json.Marshal(kv.Key) + if err != nil { + return nil, err + } + valBytes, err := json.Marshal(kv.Value) + if err != nil { + return nil, err + } + + buf.Write(keyBytes) + buf.WriteByte(':') + buf.Write(valBytes) + if i < len(pairs)-1 { + buf.WriteByte(',') + } + } + buf.WriteByte('}') + } + + return buf.Bytes(), nil +}