Files
status/api/endpoint_status.go
zeekayandHanzo Dev 78ab4f2f83 Declare operations on the group that serves them, and build on Go 1.27
zip's registration is a verb method on the group, so a route that is not a typed
operation says which method it answers with. The security package's own
two-method router goes with it: it existed so both the app and a sub-router could
satisfy one name, and restating a routing surface is what let the two disagree —
the methods it named no longer exist. The app spells itself as a group, which
registers at exactly the paths it did.

The toolchain and the pin move together, because below zip v1.36.50 the JSON
encoder was selected on a build tag: a repo whose go directive rises first writes
a wire that is neither the old release's nor the new one's until the pin catches
up.

metrics_test.go still does not compile, before and after: it compares a metric
registry against a gatherer from a different metrics library, so the two Gather
signatures disagree. That is its own change.

Co-authored-by: Hanzo Dev <[email protected]>
2026-09-17 09:58:39 -07:00

116 lines
4.7 KiB
Go

package api
import (
"encoding/json"
"errors"
"fmt"
"net/url"
"github.com/TwiN/logr"
"github.com/zap-proto/zip"
"hanzo.ai/status/client"
"hanzo.ai/status/config"
"hanzo.ai/status/config/endpoint"
"hanzo.ai/status/config/remote"
"hanzo.ai/status/storage/store"
"hanzo.ai/status/storage/store/common"
"hanzo.ai/status/storage/store/common/paging"
)
// EndpointStatuses handles requests to retrieve all EndpointStatus
// Due to how intensive this operation can be on the storage, this function leverages a cache.
func EndpointStatuses(cfg *config.Config) zip.Handler {
return func(c *zip.Ctx) error {
page, pageSize := extractPageAndPageSizeFromRequest(c, cfg.Storage.MaximumNumberOfResults)
value, exists := cache.Get(fmt.Sprintf("endpoint-status-%d-%d", page, pageSize))
var data []byte
if !exists {
endpointStatuses, err := store.Get().GetAllEndpointStatuses(paging.NewEndpointStatusParams().WithResults(page, pageSize))
if err != nil {
logr.Errorf("[api.EndpointStatuses] Failed to retrieve endpoint statuses: %s", err.Error())
return c.String(500, err.Error())
}
// ALPHA: Retrieve endpoint statuses from remote instances
if endpointStatusesFromRemote, err := getEndpointStatusesFromRemoteInstances(cfg.Remote); err != nil {
logr.Errorf("[handler.EndpointStatuses] Silently failed to retrieve endpoint statuses from remote: %s", err.Error())
} else if endpointStatusesFromRemote != nil {
endpointStatuses = append(endpointStatuses, endpointStatusesFromRemote...)
}
// Marshal endpoint statuses to JSON
data, err = json.Marshal(endpointStatuses)
if err != nil {
logr.Errorf("[api.EndpointStatuses] Unable to marshal object to JSON: %s", err.Error())
return c.String(500, "unable to marshal object to JSON")
}
cache.SetWithTTL(fmt.Sprintf("endpoint-status-%d-%d", page, pageSize), data, cacheTTL)
} else {
data = value.([]byte)
}
c.SetHeader("Content-Type", "application/json")
return c.Bytes(200, data)
}
}
func getEndpointStatusesFromRemoteInstances(remoteConfig *remote.Config) ([]*endpoint.Status, error) {
if remoteConfig == nil || len(remoteConfig.Instances) == 0 {
return nil, nil
}
var endpointStatusesFromAllRemotes []*endpoint.Status
httpClient := client.GetHTTPClient(remoteConfig.ClientConfig)
for _, instance := range remoteConfig.Instances {
response, err := httpClient.Get(instance.URL)
if err != nil {
// Log the error but continue with other instances
logr.Errorf("[api.getEndpointStatusesFromRemoteInstances] Failed to retrieve endpoint statuses from %s: %s", instance.URL, err.Error())
continue
}
var endpointStatuses []*endpoint.Status
if err = json.NewDecoder(response.Body).Decode(&endpointStatuses); err != nil {
_ = response.Body.Close()
logr.Errorf("[api.getEndpointStatusesFromRemoteInstances] Failed to decode endpoint statuses from %s: %s", instance.URL, err.Error())
continue
}
_ = response.Body.Close()
for _, endpointStatus := range endpointStatuses {
endpointStatus.Name = instance.EndpointPrefix + endpointStatus.Name
}
endpointStatusesFromAllRemotes = append(endpointStatusesFromAllRemotes, endpointStatuses...)
}
// Only return nil, error if no remote instances were successfully processed
if len(endpointStatusesFromAllRemotes) == 0 && remoteConfig.Instances != nil {
return nil, fmt.Errorf("failed to retrieve endpoint statuses from all remote instances")
}
return endpointStatusesFromAllRemotes, nil
}
// EndpointStatus retrieves a single endpoint.Status by group and endpoint name
func EndpointStatus(cfg *config.Config) zip.Handler {
return func(c *zip.Ctx) error {
page, pageSize := extractPageAndPageSizeFromRequest(c, cfg.Storage.MaximumNumberOfResults)
key, err := url.QueryUnescape(c.Param("key"))
if err != nil {
logr.Errorf("[api.EndpointStatus] Failed to decode key: %s", err.Error())
return c.String(400, "invalid key encoding")
}
endpointStatus, err := store.Get().GetEndpointStatusByKey(key, paging.NewEndpointStatusParams().WithResults(page, pageSize).WithEvents(1, cfg.Storage.MaximumNumberOfEvents))
if err != nil {
if errors.Is(err, common.ErrEndpointNotFound) {
return c.String(404, err.Error())
}
logr.Errorf("[api.EndpointStatus] Failed to retrieve endpoint status: %s", err.Error())
return c.String(500, err.Error())
}
if endpointStatus == nil { // XXX: is this check necessary?
logr.Errorf("[api.EndpointStatus] Endpoint with key=%s not found", key)
return c.String(404, "not found")
}
output, err := json.Marshal(endpointStatus)
if err != nil {
logr.Errorf("[api.EndpointStatus] Unable to marshal object to JSON: %s", err.Error())
return c.String(500, "unable to marshal object to JSON")
}
c.SetHeader("Content-Type", "application/json")
return c.Bytes(200, output)
}
}